├── public └── .gitkeep ├── storage └── .gitkeep ├── vendor └── .gitkeep ├── themes ├── default │ ├── assets │ │ └── .gitkeep │ ├── layouts │ │ └── __default__.phtml │ ├── pages │ │ └── __default__.phtml │ └── partials │ │ └── __default__.phtml └── velox │ ├── assets │ ├── css │ │ └── style.css │ ├── images │ │ ├── favicon.png │ │ ├── velox.png │ │ └── velox-logo.png │ └── js │ │ └── script.js │ ├── partials │ ├── components │ │ ├── paragraph.phtml │ │ ├── heading.phtml │ │ ├── link.phtml │ │ ├── button.phtml │ │ ├── image.phtml │ │ └── figure.phtml │ ├── page │ │ ├── header.phtml │ │ └── footer.phtml │ ├── meta │ │ ├── scripts.phtml │ │ ├── stylesheets.phtml │ │ └── metadata.phtml │ ├── text.phtml │ ├── text-image.phtml │ ├── hero.phtml │ ├── message.phtml │ ├── form.phtml │ └── includes │ │ └── navigation.phtml │ ├── layouts │ ├── base.phtml │ ├── simple.phtml │ └── main.phtml │ └── pages │ ├── error │ ├── 401.phtml │ ├── 403.phtml │ ├── 500.phtml │ ├── 404.phtml │ └── 405.phtml │ ├── dump.phtml │ ├── contact.phtml │ ├── users │ ├── login.phtml │ ├── index.phtml │ └── register.phtml │ ├── home.phtml │ └── persons │ ├── create.phtml │ ├── show.phtml │ ├── edit.phtml │ └── index.phtml ├── .gitignore ├── index.php ├── bin ├── config-cache ├── cache-clear ├── config-dump ├── app-serve └── app-mirror ├── .editorconfig ├── bootstrap ├── additional.php ├── intellisense.php ├── autoload.php └── loader.php ├── config ├── auth.php ├── database.php ├── theme.php ├── router.php ├── session.php ├── data.php ├── cli.php ├── view.php └── global.php ├── LICENSE ├── app ├── Model │ └── Person.php └── Controller │ ├── DefaultController.php │ ├── UsersController.php │ └── PersonsController.php ├── functions └── html.php ├── .htaccess.dist ├── composer.json ├── includes ├── events │ └── system.php └── routes │ └── web.php └── classes ├── Frontend ├── Data.php ├── View │ └── Compiler.php └── Path.php └── Backend ├── Session ├── CSRF.php └── Flash.php ├── Event.php ├── Model └── Element.php ├── Session.php ├── Exception.php ├── Config.php ├── Controller.php ├── Database.php └── Auth.php /public/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /storage/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /themes/default/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /themes/velox/assets/css/style.css: -------------------------------------------------------------------------------- 1 | @import url('https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css'); -------------------------------------------------------------------------------- /themes/velox/partials/components/paragraph.phtml: -------------------------------------------------------------------------------- 1 | $class ?? false, 3 | ]) ?> 4 | -------------------------------------------------------------------------------- /themes/velox/assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarwanAlsoltany/velox/HEAD/themes/velox/assets/images/favicon.png -------------------------------------------------------------------------------- /themes/velox/assets/images/velox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarwanAlsoltany/velox/HEAD/themes/velox/assets/images/velox.png -------------------------------------------------------------------------------- /themes/velox/partials/page/header.phtml: -------------------------------------------------------------------------------- 1 |
2 | 3 |
-------------------------------------------------------------------------------- /themes/velox/assets/images/velox-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarwanAlsoltany/velox/HEAD/themes/velox/assets/images/velox-logo.png -------------------------------------------------------------------------------- /themes/velox/partials/components/heading.phtml: -------------------------------------------------------------------------------- 1 | $class ?? false, 3 | ]); 4 | -------------------------------------------------------------------------------- /themes/velox/partials/components/link.phtml: -------------------------------------------------------------------------------- 1 | $href, 3 | 'class' => $class ?? false, 4 | ]) ?> 5 | -------------------------------------------------------------------------------- /themes/velox/partials/meta/scripts.phtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /themes/velox/partials/components/button.phtml: -------------------------------------------------------------------------------- 1 | $href ?? false, 3 | 'class' => 'button ' . $class, 4 | ]) ?> 5 | -------------------------------------------------------------------------------- /themes/velox/partials/meta/stylesheets.phtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /themes/velox/partials/components/image.phtml: -------------------------------------------------------------------------------- 1 | $src, 3 | 'alt' => $alt ?? false, 4 | 'class' => $class ?? false, 5 | 'loading' => 'lazy' 6 | ]) ?> 7 | -------------------------------------------------------------------------------- /themes/velox/layouts/base.phtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /themes/default/layouts/__default__.phtml: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 |
6 | 7 | -------------------------------------------------------------------------------- /themes/velox/pages/error/401.phtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ($code ?? 401) . ' ¯\_( ͠❛ ͟ʖ͠❛ )_/¯', 5 | 'subtitle' => $message ?? 'Unauthorized!', 6 | 'class' => 'is-link is-large is-bold' 7 | ]) ?> 8 | -------------------------------------------------------------------------------- /themes/velox/partials/components/figure.phtml: -------------------------------------------------------------------------------- 1 | $src ?? false, 4 | 'alt' => $alt ?? false, 5 | ]). 6 | HTML::figcaption($caption ?? ''), 7 | [ 8 | 'class' => $class ?? false 9 | ] 10 | ) ?> 11 | -------------------------------------------------------------------------------- /themes/velox/pages/error/403.phtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ($code ?? 403) . ' ¯\_( ͠❛ ͜ʖ͠❛ )_/¯', 5 | 'subtitle' => $message ?? 'Invalid CSRF Token!', 6 | 'class' => 'is-danger is-large is-bold' 7 | ]) ?> 8 | -------------------------------------------------------------------------------- /themes/velox/pages/error/500.phtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ($code ?? 500) . ' ¯\_( ͠❛ ͟ʖ͠❛ )_/¯', 5 | 'subtitle' => $message ?? 'An error occurred, try again later.', 6 | 'class' => 'is-danger is-large is-bold' 7 | ]) ?> 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .idea 3 | 4 | server.php 5 | test.php 6 | public/* 7 | storage/* 8 | vendor/* 9 | build/* 10 | !**/.gitkeep 11 | 12 | .ht* 13 | !.ht*.* 14 | 15 | *.bak 16 | *.cache 17 | *.lock 18 | *.xml 19 | *.log 20 | *.sql 21 | *.sqlite 22 | *.db 23 | .Trashes 24 | .DS_Store 25 | .DS_Store? 26 | .Spotlight-V100 27 | ._* 28 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Marwan Al-Soltany 2021 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | require 'bootstrap/autoload.php'; 13 | -------------------------------------------------------------------------------- /themes/velox/pages/dump.phtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /bin/config-cache: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 2 | 3 | ($code ?? 404) . ' ¯\_( ͠❛  ͟ʖ͡❛ )_/¯', 5 | 'subtitle' => $message ?? sprintf( 6 | 'The requested page “%s” was not found!', 7 | hse(Globals::getServer('REQUEST_URI')) 8 | ), 9 | 'class' => 'is-info is-large is-bold' 10 | ]) ?> 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; This file is for unifying the coding style for different editors and IDEs. 2 | ; More information at https://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | indent_size = 4 9 | indent_style = space 10 | end_of_line = lf 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | [*.{js,css,scss}] 15 | indent_size = 2 16 | 17 | [*.md] 18 | trim_trailing_whitespace = false 19 | -------------------------------------------------------------------------------- /themes/velox/layouts/simple.phtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <?= Data::get('page.title', 'Page') ?> 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /themes/velox/pages/error/405.phtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ($code ?? 405) . ' ¯\_( ͠❛ ͜ʖ ͡❛)_/¯', 5 | 'subtitle'=> $message ?? sprintf( 6 | 'The requested route “%s” exists. But the request method “%s” is not allowed!', 7 | hse(Globals::getServer('REQUEST_URI')), 8 | hse(Globals::getServer('REQUEST_METHOD')) 9 | ), 10 | 'class' => 'is-warning is-large is-bold' 11 | ]) ?> 12 | -------------------------------------------------------------------------------- /themes/velox/partials/text-image.phtml: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 | 7 |
8 |
9 | 10 |
11 |
12 |
13 |
-------------------------------------------------------------------------------- /themes/velox/partials/page/footer.phtml: -------------------------------------------------------------------------------- 1 |
2 |

3 | VELOX by Marwan Al-Soltany.
4 | The source code is licensed under MIT license. 5 |

6 |

Check out the project on GitHub

7 |

©

8 |
9 | -------------------------------------------------------------------------------- /bootstrap/additional.php: -------------------------------------------------------------------------------- 1 | "> 2 |
3 |
4 | $title, 6 | 'level' => 1, 7 | 'class' => 'title is-1 is-spaced' 8 | ]) ?> 9 | $subtitle, 11 | 'level' => 2, 12 | 'class' => 'subtitle' 13 | ]) ?> 14 |
15 |
16 | 17 | -------------------------------------------------------------------------------- /bin/config-dump: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 3 |
4 |
5 |
6 |
7 | 8 |
9 |

10 |
11 | 12 |
13 | 14 |
15 |
16 |
17 |
18 |
19 | 20 | -------------------------------------------------------------------------------- /config/auth.php: -------------------------------------------------------------------------------- 1 | [ 16 | // The auth user model class FQN. If left empty it will use the default auth user model. 17 | // If specified, this class has to have at least the "id", "username", and "password" attributes. 18 | 'model' => null, 19 | 20 | // The timeout in seconds before the user is automatically logged out. 21 | 'timeout' => (1 * 60 * 60), // 1 hour 22 | ], 23 | 24 | // Password hashing config. 25 | 'hashing' => [ 26 | 'algorithm' => PASSWORD_DEFAULT, 27 | 'cost' => 11, 28 | ], 29 | 30 | 31 | ]; 32 | -------------------------------------------------------------------------------- /config/database.php: -------------------------------------------------------------------------------- 1 | 'mysql', 16 | 'host' => 'localhost', 17 | 'port' => 3306, 18 | 'charset' => 'utf8mb4', 19 | 'dbname' => 'velox', 20 | 'username' => 'root', 21 | 'password' => '', 22 | 23 | // Database DSN (Data Source Name) of the connection. 24 | // Populate the DSN using the the provided credentials above. 25 | // See https://www.php.net/manual/en/pdo.drivers.php for more info about drivers DSN. 26 | 'dsn' => '{database.driver}:host={database.host};port={database.port};dbname={database.dbname};charset={database.charset}', 27 | 28 | 29 | ]; 30 | -------------------------------------------------------------------------------- /themes/velox/assets/js/script.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", () => { 2 | // Get all "navbar-burger" elements 3 | const $navbarBurgers = Array.prototype.slice.call( 4 | document.querySelectorAll(".navbar-burger"), 5 | 0 6 | ); 7 | 8 | // Check if there are any navbar burgers 9 | if ($navbarBurgers.length > 0) { 10 | // Add a click event on each of them 11 | $navbarBurgers.forEach((el) => { 12 | el.addEventListener("click", () => { 13 | // Get the target from the "data-target" attribute 14 | const target = el.dataset.target; 15 | const $target = document.getElementById(target); 16 | 17 | // Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu" 18 | el.classList.toggle("is-active"); 19 | $target.classList.toggle("is-active"); 20 | }); 21 | }); 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /config/theme.php: -------------------------------------------------------------------------------- 1 | 'velox', 19 | 20 | 21 | // Theme parent(s), array or string. Used to inherit view files from. 22 | 'parent' => 'default', 23 | 24 | 25 | // Theme directory structure. 26 | 'paths' => [ 27 | 'root' => '{global.paths.themes}/{theme.active}', 28 | 'assets' => '{theme.paths.root}/assets', 29 | 'layouts' => '{theme.paths.root}/layouts', 30 | 'pages' => '{theme.paths.root}/pages', 31 | 'partials' => '{theme.paths.root}/partials', 32 | ], 33 | 34 | 35 | ]; 36 | -------------------------------------------------------------------------------- /config/router.php: -------------------------------------------------------------------------------- 1 | Router::DEFAULTS['base'], 16 | 17 | 18 | // Whether to allow for multiple route matching or not. 19 | 'allowMultiMatch' => Router::DEFAULTS['allowMultiMatch'], 20 | 21 | 22 | // Whether route matching should be case sensitive or not. 23 | 'caseMatters' => Router::DEFAULTS['caseMatters'], 24 | 25 | 26 | // Whether trailing slashes in the route matter or not. 27 | 'slashMatters' => Router::DEFAULTS['slashMatters'], 28 | 29 | 30 | // Whether to start the router automatically without the need for calling `Router::start()` or not. 31 | 'allowAutoStart' => Router::DEFAULTS['allowAutoStart'], 32 | 33 | 34 | ]; 35 | -------------------------------------------------------------------------------- /themes/velox/partials/meta/metadata.phtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <?= Data::get('page.title', 'Untitled') ?> 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /themes/velox/layouts/main.phtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 |
15 | 16 |
17 | 18 |
19 | 20 | 23 | 24 | 'has-background-warning p-2 has-text-centered']) 26 | : '' 27 | ?> 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Marwan Al-Soltany 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /bootstrap/intellisense.php: -------------------------------------------------------------------------------- 1 | '{global.paths.storage}/app/sessions', 16 | 17 | 18 | // PHP session cache configuration. 19 | 'cache' => [ 20 | // Cache limiter, see https://www.php.net/manual/en/function.session-cache-limiter 21 | 'limiter' => null, 22 | // Cache expiration, see https://www.php.net/manual/en/function.session-cache-expire 23 | 'expiration' => null, 24 | ], 25 | 26 | 27 | // CSRF protection. 28 | 'csrf' => [ 29 | // Input field name that contains the CSRF token. 30 | 'name' => '_token', 31 | // HTTP methods that should be checked against CSRF. 32 | 'methods' => ['POST', 'PUT', 'PATCH', 'DELETE'], 33 | // Whitelisted hosts and/or IPs that are allowed to pass CSRF check (Hostname has precedence over IP). 34 | 'whitelisted' => [ 35 | // 'https://domain.tld' 36 | // '127.0.0.1' 37 | ], 38 | ], 39 | 40 | 41 | ]; 42 | -------------------------------------------------------------------------------- /config/data.php: -------------------------------------------------------------------------------- 1 | [ 19 | // # example (extend or change the definition to your liking) 20 | // [ 21 | // 'route' => '/', 22 | // 'method' => 'GET', 23 | // 'page' => 'home', 24 | // 'layout' => 'base', 25 | // 'variables' => [ 26 | // 'title' => 'Home', 27 | // // ... 28 | // ], 29 | // ], 30 | 31 | // # add more pages ... 32 | ], 33 | 34 | 35 | 'menus' => [ 36 | // # example (extend or change the definition to your liking) 37 | // 'main' => [ 38 | // [ 39 | // 'name' => 'Home', 40 | // 'route' => '/', 41 | // 'title' => 'Homepage' 42 | // ] 43 | // ] 44 | 45 | // # add more menus ... 46 | ] 47 | 48 | 49 | ]; 50 | -------------------------------------------------------------------------------- /bin/app-serve: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | Config::get('cli.app-serve.args.host', 'localhost'), 17 | 'port' => Config::get('cli.app-serve.args.port', 8000), 18 | 'root' => Config::get('cli.app-serve.args.root', dirname(__DIR__)), 19 | 'router' => Config::get('cli.app-serve.args.root', dirname(__DIR__)) . '/server.php', 20 | ]; 21 | 22 | $command = vsprintf('php -S %s:%d -t %s %s', $server); 23 | 24 | 25 | $routerFile = $server['router']; 26 | $routerContent = << 2 | 3 | 'Contact', 5 | 'subtitle'=> 'Check out the form down below.', 6 | 'class' => 'is-medium is-primary is-bold' 7 | ]) ?> 8 | 9 | [ 11 | 'content' => 'Cool Stuff!', 12 | 'class' => 'title', 13 | ], 14 | 'paragraph' => [ 15 | 'content' => 'As you have guessed by now, the main idea of VELOX is to keep it as simple as it can be.
Often when creating small websites, you normally have a few pages to make that will hardly get updated, you can generate and cache these pages as static HTML files and serve them on demand. For other pages like this one, you have to keep it dynamic, VELOX gives you the ability to do that and a lot more other cool stuff.' 16 | ], 17 | ]) ?> 18 | 19 | $message ?? null, 21 | ]) ?> 22 | 23 | [ 25 | 'content' => 'Keep it concise. Keep it VELOX!', 26 | 'class' => 'title', 27 | ], 28 | 'paragraph' => [ 29 | 'content' => 'Keep it concise. DRY! Although it is simple, VELOX does not compromise on flexibility and scalability. All content you have seen till now consists of a bunch of reusable pieces of code in form of Partials, Pages, and Layouts, in addition to that, there are also themes that can inherit from each other. VELOX provides a lot of helpers that are more than sufficient to do the job it was intended to do.' 30 | ], 31 | 'image' => [ 32 | 'src' => Path::resolveUrlFromAssets('images/velox.png'), 33 | 'alt' => 'VELOX', 34 | 'caption' => 'Lorem ipsum dolor sit amet' 35 | ] 36 | ]) ?> 37 | -------------------------------------------------------------------------------- /bootstrap/autoload.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright Marwan Al-Soltany 2021 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Model; 13 | 14 | use MAKS\Velox\Backend\Model; 15 | 16 | class Person extends Model 17 | { 18 | public static function schema(): string 19 | { 20 | return vsprintf(' 21 | CREATE TABLE IF NOT EXISTS `%s` ( 22 | `%s` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, 23 | `name` VARCHAR(255) NOT NULL, 24 | `age` INT, 25 | `username` VARCHAR(255) NOT NULL, 26 | `email` VARCHAR(255), 27 | `address` VARCHAR(255), 28 | `company` VARCHAR(255) 29 | ); 30 | ', [ 31 | static::getTable(), 32 | static::getPrimaryKey(), 33 | ]); 34 | } 35 | 36 | public static function getTable(): string 37 | { 38 | return 'persons'; 39 | // or overwrite `self::$table` property instead. 40 | } 41 | 42 | public static function getColumns(): array 43 | { 44 | return [ 45 | 'id', 46 | 'name', 47 | 'age', 48 | 'username', 49 | 'email', 50 | 'address', 51 | 'company', 52 | ]; 53 | // or overwrite `self::$columns` property instead. 54 | } 55 | 56 | public static function getPrimaryKey(): string 57 | { 58 | return 'id'; 59 | // or overwrite `self::$primaryKey` property instead. 60 | } 61 | 62 | protected function bootstrap(): void 63 | { 64 | // add your own bootstrap logic here 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/Controller/DefaultController.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright Marwan Al-Soltany 2021 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Controller; 13 | 14 | use MAKS\Velox\Backend\Controller; 15 | 16 | class DefaultController extends Controller 17 | { 18 | /** 19 | * Example action. 20 | * 21 | * @param string $path The current path matched by the router i.e. `/page/1`. 22 | * @param string|null $match The match from the route if there was any (`/page/{id}` -> `$match` = `id`). 23 | * @param mixed|null $previous The result of the last middleware or route with a matching path. 24 | * 25 | * @return string 26 | */ 27 | public function exampleAction(string $path, ?string $match, $previous) 28 | { 29 | /** 30 | * This is an example to show you how to work with VELOX controllers. 31 | * What is written here is only for demonstration purposes. 32 | * Normally, here you should only process the data and pass them to some view. 33 | * 34 | * You may delete this file or change directory structure in "./app" as you wish, only do not forget to follow PSR-4. 35 | */ 36 | 37 | $this->data->set('page.title', 'Example'); 38 | 39 | $this->view->section( 40 | $this->config->get('view.defaultSectionName'), 41 | $this->view->partial('hero', [ 42 | 'title'=> 'Hi there,', 43 | 'subtitle'=> 'This is the response from the ' . __METHOD__ 44 | ]) 45 | ); 46 | 47 | return $this->view->render( 48 | $this->config->get('view.defaultPageName'), 49 | $this->config->get('view.defaultPageVars'), 50 | $this->config->get('view.defaultLayoutName') 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /config/cli.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'enabled' => true, 15 | 'args' => [ 16 | 'host' => 'localhost', 17 | 'port' => 8000, 18 | 'root' => '{global.paths.root}', 19 | ], 20 | ], 21 | 22 | 23 | // This command clears the cache of the configuration and/or views. 24 | 'cache-clear' => [ 25 | 'enabled' => true, 26 | 'args' => [ 27 | 'config' => true, 28 | 'views' => true, 29 | ], 30 | ], 31 | 32 | 33 | // This command caches the current configuration. 34 | 'config-cache' => [ 35 | 'enabled' => true, 36 | ], 37 | 38 | 39 | // This command dumps the current configuration with syntax highlighting. 40 | 'config-dump' => [ 41 | 'enabled' => true, 42 | 'args' => [ 43 | 'parse' => true, 44 | ], 45 | ], 46 | 47 | 48 | // This command mirrors (symlinks/copies) the provided files/directories in {global.paths.public}. 49 | 'app-mirror' => [ 50 | 'enabled' => true, 51 | 'args' => [ 52 | // Files/directories to link. Key is the link, value is the target. 53 | // Providing no key will create the necessary directories to reflect the target path. 54 | 'link' => [ 55 | '{global.paths.root}/index.php', 56 | '{theme.paths.assets}', 57 | ], 58 | // Files/directories to copy. Key is the destination, value is the source. 59 | // Providing no key will create the necessary directories to reflect the source path. 60 | 'copy' => [ 61 | 'html' => '{global.paths.storage}/cache/views', 62 | ], 63 | ], 64 | ], 65 | 66 | 67 | ]; 68 | -------------------------------------------------------------------------------- /config/view.php: -------------------------------------------------------------------------------- 1 | View::DEFAULTS['fileExtension'], 16 | 17 | 18 | // Whether to inherit view files from parent theme. 19 | 'inherit' => View::DEFAULTS['inherit'], 20 | 21 | 22 | // Whether to minify the rendered views. 23 | 'minify' => View::DEFAULTS['minify'], 24 | 25 | 26 | // Whether to cache the rendered views as static HTML files. 27 | 'cache' => View::DEFAULTS['cache'], 28 | // An array or string of pages that should never be cached (useful for dynamic pages). 29 | 'cacheExclude' => View::DEFAULTS['cacheExclude'], 30 | // Whether to cache the views in nested directories as "index.html" or as file with an autogenerated name based on its path. 31 | 'cacheAsIndex' => View::DEFAULTS['cacheAsIndex'], 32 | // Whether to put a comment with a timestamp in the cached view file. 33 | 'cacheWithTimestamp' => View::DEFAULTS['cacheWithTimestamp'], 34 | 35 | 36 | // The default names of files VELOX falls back to when rendering a view. 37 | 'defaultLayoutName' => 'main', // View::DEFAULTS['name'], 38 | 'defaultPageName' => View::DEFAULTS['name'], 39 | 'defaultPartialName' => View::DEFAULTS['name'], 40 | 'defaultSectionName' => View::DEFAULTS['name'], 41 | 42 | 43 | // The variables passed to all Layouts, Pages, or Partials by default, they will be passed as an array under "default*Vars". 44 | 'defaultLayoutVars' => View::DEFAULTS['variables'], 45 | 'defaultPageVars' => View::DEFAULTS['variables'], 46 | 'defaultPartialVars' => View::DEFAULTS['variables'], 47 | 48 | 49 | // Templating engine configuration. 50 | 'engine' => View::DEFAULTS['engine'], 51 | 52 | 53 | ]; 54 | -------------------------------------------------------------------------------- /themes/velox/pages/users/login.phtml: -------------------------------------------------------------------------------- 1 | {! @extends 'velox/pages/users/index' !} 2 | 3 | 4 | {! @block content !} 5 |
6 |
7 |
8 |
9 |

{{ $title }}

10 |

Don't have an account? Register a new one.

11 |
12 |
13 | 14 | 15 |
16 | 17 |
18 | 29 |
30 |
31 |
32 | 33 |
34 | 35 |
36 |
37 |
38 | 39 |
40 |
41 |
42 |
43 |
44 | {! @endblock !} 45 | -------------------------------------------------------------------------------- /themes/velox/pages/users/index.phtml: -------------------------------------------------------------------------------- 1 | {! Data::set('page.title', $title ?? 'Users') !} 2 | 3 |
4 | {{{ View::partial('hero', [ 5 | 'title' => 'Users', 6 | 'subtitle' => 'Auth Demo', 7 | 'class' => 'is-small is-primary is-bold' 8 | ]) }}} 9 |
10 |
11 |
12 |
13 | {! @block flashMessages !} 14 |
15 | {{{ Session::flash() }}} 16 |
17 | {! @endblock!} 18 | {! @block(flashMessages) !} 19 | 20 | {! @block content !} 21 |
22 |
23 |
24 |
25 |

Auth

26 |

You are currently logged in as {{ $user->username }}

27 |

You won't see this page unless you are authenticated.

28 |

Try logging out and visiting the following URL: {{ Path::currentUrl() }}.

29 |

Log out

30 |
31 |

Would you like to delete your account?

32 |

Unregister

33 |
34 |
35 |
36 |
37 | {! @endblock !} 38 | {! @block(content) !} 39 |
40 |
41 |
42 |
43 |
44 | -------------------------------------------------------------------------------- /functions/html.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright Marwan Al-Soltany 2021 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | 13 | 14 | if (!function_exists('he')) { 15 | /** 16 | * Encodes the passed text with `htmlentities()`. 17 | * 18 | * @param string $text 19 | * 20 | * @return string 21 | */ 22 | function he($text) { 23 | return htmlentities((string)$text, ENT_QUOTES, 'UTF-8'); 24 | } 25 | } 26 | 27 | if (!function_exists('hd')) { 28 | /** 29 | * Decodes the passed text with `html_entity_decode()`. 30 | * 31 | * @param string $text 32 | * 33 | * @return string 34 | */ 35 | function hd($text) { 36 | return html_entity_decode((string)$text, ENT_QUOTES, 'UTF-8'); 37 | } 38 | } 39 | 40 | if (!function_exists('hse')) { 41 | /** 42 | * Encodes the passed text with `htmlspecialchars()`. 43 | * 44 | * @param string $text 45 | * 46 | * @return string 47 | */ 48 | function hse($text) { 49 | return htmlspecialchars((string)$text, ENT_QUOTES, 'UTF-8'); 50 | } 51 | } 52 | 53 | if (!function_exists('hsd')) { 54 | /** 55 | * Decodes the passed text with `htmlspecialchars_decode()`. 56 | * 57 | * @param string $text 58 | * 59 | * @return string 60 | */ 61 | function hsd($text) { 62 | return htmlspecialchars_decode((string)$text, ENT_QUOTES, 'UTF-8'); 63 | } 64 | } 65 | 66 | if (!function_exists('st')) { 67 | /** 68 | * Strips the passed text with `strip_tags()`. 69 | * 70 | * @param string $text 71 | * 72 | * @return string 73 | */ 74 | function st($text) { 75 | return strip_tags((string)$text); 76 | } 77 | } 78 | 79 | if (!function_exists('nb')) { 80 | /** 81 | * Turns `\n` to `
` in the passed text with `nl2br()`. 82 | * 83 | * @param string $text 84 | * 85 | * @return string 86 | */ 87 | function nb($text) { 88 | return nl2br((string)$text); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /themes/velox/pages/home.phtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 'Welcome to VELOX', 5 | 'subtitle' => 'The fastest way to build simple websites using PHP!' 6 | ]) ?> 7 | 8 | [ 10 | 'content' => 'About VELOX', 11 | 'class' => 'title', 12 | ], 13 | 'paragraph' => [ 14 | 'content' => 'VELOX is a lightweight micro-framework that makes creating a simple website using PHP joyful. It helps you create future-proof websites faster and more efficiently. It provides components that facilitate the process of creating a website using vanilla PHP. VELOX does not have any dependencies, the VELOX package and everything that it needs is included in the project itself. VELOX can be used as a Static Site Generator if all you need is HTML files in the end.', 15 | ], 16 | ]) ?> 17 | 18 | 'Why does VELOX exist?', 20 | 'body' => 'VELOX was created to solve a specific problem, it\'s a way to build a website that is between dynamic and static, a way to create a simple website with few pages without being forced to use a framework or a CMS that comes with a ton of stuff which will never get used, it\'s lightweight and straight to the point.', 21 | 'class' => 'is-success', 22 | ]) ?> 23 | 24 | 'What is the use-case for VELOX?', 26 | 'body' => 'VELOX has a very special use-case, simple websites, and here is meant really simple websites. The advantage is, you don\'t have stuff that you don\'t need.', 27 | 'class' => 'is-warning', 28 | ]) ?> 29 | 30 |
31 |
32 |
33 |
34 | Path::resolveUrlFromAssets('images/velox-logo.png'), 36 | 'alt' => 'VELOX', 37 | 'class' => 'has-text-centered', 38 | 'caption' => 'VELOX is a Latin word that stands for “Swift”.' 39 | ]) ?> 40 |
41 |
42 |
43 |
44 | -------------------------------------------------------------------------------- /.htaccess.dist: -------------------------------------------------------------------------------- 1 | ## 2 | ## BasicAuth for development purposes 3 | ## 4 | # AuthType Basic 5 | # AuthName "Say my name!" 6 | # AuthUserFile /ABSOLUTE/PATH/TO/.htpasswd 7 | # Require valid-user 8 | 9 | 10 | DirectoryIndex index.php 11 | 12 | 13 | 14 | 15 | 16 | Options -MultiViews 17 | 18 | 19 | 20 | Options -Indexes 21 | 22 | 23 | Options +SymLinksIfOwnerMatch 24 | 25 | # Use UTF-8 encoding for anything served text/plain or text/html 26 | AddDefaultCharset UTF-8 27 | 28 | ## 29 | ## Enable apache rewrite engine 30 | ## 31 | RewriteEngine On 32 | 33 | ## 34 | ## You may need to uncomment the following line for some hosting environments, 35 | ## if you have installed to a subdirectory, enter the name here also. 36 | ## 37 | # RewriteBase / 38 | 39 | ## 40 | ## Uncomment following lines to force HTTPS. 41 | ## 42 | # RewriteCond %{HTTPS} off 43 | # RewriteRule (.*) https://%{SERVER_NAME}/$1 [L,R=301] 44 | 45 | ## 46 | ## Black listed folders 47 | ## 48 | RewriteRule ^bootstrap/.* index.php [L,NC] 49 | RewriteRule ^classes/.* index.php [L,NC] 50 | RewriteRule ^functions/* index.php [L,NC] 51 | RewriteRule ^includes/* index.php [L,NC] 52 | RewriteRule ^app/.* index.php [L,NC] 53 | RewriteRule ^config/.* index.php [L,NC] 54 | RewriteRule ^tests/.* index.php [L,NC] 55 | 56 | ## 57 | ## White listed folders 58 | ## 59 | RewriteCond %{REQUEST_FILENAME} -f 60 | RewriteCond %{REQUEST_FILENAME} !/.well-known/* 61 | RewriteCond %{REQUEST_FILENAME} !/(uploads|storage|public)/.* 62 | RewriteCond %{REQUEST_FILENAME} !/themes/.*/(assets|resources|public)/.* 63 | RewriteRule !^index.php index.php [L,NC] 64 | 65 | # restrict access to dot files 66 | RewriteCond %{REQUEST_FILENAME} -d [OR] 67 | RewriteCond %{REQUEST_FILENAME} -l [OR] 68 | RewriteCond %{REQUEST_FILENAME} -f 69 | RewriteRule /\.|^\.(?!well-known/) - [F,L] 70 | 71 | ## 72 | ## Block all PHP files, except app index 73 | ## 74 | RewriteCond %{REQUEST_FILENAME} -f 75 | RewriteCond %{REQUEST_FILENAME} \.php$ 76 | RewriteRule !^index.php index.php [L,NC] 77 | 78 | ## 79 | ## Standard routes 80 | ## 81 | # Deliver the folder or file directly if it exists on the server 82 | RewriteCond %{REQUEST_FILENAME} !-f 83 | RewriteCond %{REQUEST_FILENAME} !-d 84 | # Push every request to index.php 85 | RewriteRule ^(.*)$ /index.php [QSA] 86 | 87 | 88 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "marwanalsoltany/velox", 3 | "type": "project", 4 | "license": "MIT", 5 | "description": "The minimal PHP micro-framework.", 6 | "keywords": [ 7 | "php", 8 | "framework", 9 | "micro-framework", 10 | "mvc", 11 | "crud", 12 | "auth", 13 | "static-website-generator" 14 | ], 15 | "authors": [ 16 | { 17 | "name": "Marwan Al-Soltany", 18 | "email": "MarwanAlsoltany+gh@gmail.com" 19 | } 20 | ], 21 | "funding": [ 22 | { 23 | "type": "ko-fi", 24 | "url": "https://ko-fi.com/marwanalsoltany" 25 | } 26 | ], 27 | "homepage": "https://github.com/MarwanAlsoltany/velox/blob/master/README.md", 28 | "support": { 29 | "docs": "https://marwanalsoltany.github.io/velox", 30 | "source": "https://github.com/MarwanAlsoltany/velox", 31 | "issues": "https://github.com/MarwanAlsoltany/velox/issues" 32 | }, 33 | "require": { 34 | "php": "^7.4 || ^8.0", 35 | "ext-mbstring": "*", 36 | "ext-json": "*", 37 | "ext-dom": "*", 38 | "ext-intl": "*", 39 | "ext-pdo": "*" 40 | }, 41 | "require-dev": { 42 | "marwanalsoltany/blend": "^1.0", 43 | "phpunit/phpunit": "^9.5" 44 | }, 45 | "autoload": { 46 | "psr-4": { 47 | "MAKS\\Velox\\": "classes", 48 | "App\\": "app" 49 | } 50 | }, 51 | "autoload-dev": { 52 | "psr-4": { 53 | "MAKS\\Velox\\Tests\\": "tests" 54 | } 55 | }, 56 | "extra": { 57 | "branch-alias": { 58 | "dev-master": "1.5-dev" 59 | } 60 | }, 61 | "scripts": { 62 | "test": "phpunit", 63 | "document": "([ -f ./phpDocumentor.phar ] && php phpDocumentor.phar) || echo phpDocumentor.phar is not available in CWD. Download using [php -r \"copy('https://phpdoc.org/phpDocumentor.phar', 'phpDocumentorNew.phar');\"]", 64 | "build": [ 65 | "@test", 66 | "@document" 67 | ], 68 | "build-dev": [ 69 | "composer run-script build --dev --verbose", 70 | "echo ! && echo ! Development build completed! && echo !" 71 | ], 72 | "build-prod": [ 73 | "composer run-script build --quiet", 74 | "echo ! && echo ! Production build completed! && echo !" 75 | ] 76 | }, 77 | "config": { 78 | "optimize-autoloader": true, 79 | "sort-packages": false, 80 | "process-timeout": 0 81 | }, 82 | "prefer-stable": true 83 | } 84 | -------------------------------------------------------------------------------- /config/global.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'root' => BASE_PATH, 15 | 'app' => '{global.paths.root}/app', 16 | 'classes' => '{global.paths.root}/classes', 17 | 'functions' => '{global.paths.root}/functions', 18 | 'includes' => '{global.paths.root}/includes', 19 | 'themes' => '{global.paths.root}/themes', 20 | 'config' => '{global.paths.root}/config', 21 | 'storage' => '{global.paths.root}/storage', 22 | 'public' => '{global.paths.root}/public', 23 | ], 24 | 25 | 26 | // VELOX current environment, [DEV = development, PROD = production]. 27 | 'env' => 'DEV', 28 | 29 | 30 | // VELOX base URL, overrides server default base URL. 31 | 'baseUrl' => null, 32 | 33 | 34 | // VELOX default timezone, this will be used by all date/time functions. 35 | 'timezone' => 'Europe/Berlin', 36 | 37 | 38 | // VELOX error pages, view files to render for HTTP errors. 39 | // The view set here will also be rendered when calling "App::abort($code)" instead of the default page. 40 | 'errorPages' => [ 41 | // A view file for "500 Internal Server Error" responses, if an uncaught exception was thrown in production environment. 42 | '500' => 'error/500', 43 | // A view file for "401 Unauthorized" responses, if a unauthenticated user requested a page that requires authentication. 44 | '401' => 'error/401', 45 | // A view file for "403 Forbidden" responses, if request CSRF token is invalid. 46 | '403' => 'error/403', 47 | // A view file for "404 Not Found" responses, if requested route was not found. 48 | '404' => 'error/404', 49 | // A view file for "405 Not Allowed" responses, if requested method is not allowed. 50 | '405' => 'error/405', 51 | ], 52 | 53 | 54 | // VELOX logging configuration. 55 | 'logging' => [ 56 | // Whether or not to enable logging of different events of the app. Note that exception will be logged no matter what the value here is. 57 | 'enabled' => true, 58 | // The maximum file size of the log file in bytes before it gets truncated. 59 | 'maxFileSize' => 6.4e+7, 60 | // The default file name for logs without explicitly given file name on function call. 61 | 'defaultFilename' => 'autogenerated-' . date('Ymd'), 62 | // The default writing directory for logs without explicitly given writing directory on function call. 63 | 'defaultDirectory' => '{global.paths.storage}/logs/', 64 | ], 65 | 66 | 67 | ]; 68 | -------------------------------------------------------------------------------- /themes/velox/pages/users/register.phtml: -------------------------------------------------------------------------------- 1 | {! @extends 'velox/pages/users/index' !} 2 | 3 | 4 | {! @block content !} 5 |
6 |
7 |
8 |

Create a new Account

9 |

Already have an account? Login to your account.

10 |
11 |
12 |
13 | 14 | {{{ Session::csrf() }}} 15 |
16 | 17 |
18 | 29 | 30 | Username can contain 31 | letters, 32 | numbers 33 | and one of the following characters 34 | . 35 | - 36 | _ 37 | 38 |
39 |
40 |
41 | 42 |
43 | 44 |
45 |
46 |
47 |
48 |
49 | 50 |
51 |
52 | 53 |
54 |
55 | Cancel 56 |
57 |
58 |
59 |
60 |
61 | {! @endblock !} 62 | -------------------------------------------------------------------------------- /themes/velox/pages/persons/create.phtml: -------------------------------------------------------------------------------- 1 | {! @extends 'velox/pages/persons/index' !} 2 | 3 | 4 | {! @block navigationItems !} 5 |
  • List
  • 6 | {! @endblock !} 7 | 8 | {! @block content !} 9 |
    10 |
    11 |

    Create a new Person

    12 |
    13 | 14 | {{{ Session::csrf() }}} 15 |
    16 | 17 |
    18 | 19 |
    20 |
    21 |
    22 | 23 |
    24 | 25 |
    26 |
    27 |
    28 | 29 |
    30 | 31 |
    32 |
    33 |
    34 | 35 |
    36 | 37 |
    38 |
    39 |
    40 | 41 |
    42 | 43 |
    44 |
    45 |
    46 | 47 |
    48 | 49 |
    50 |
    51 |
    52 |
    53 |
    54 | 55 |
    56 |
    57 | 58 |
    59 |
    60 | Cancel 61 |
    62 |
    63 |
    64 |
    65 |
    66 | {! @endblock !} 67 | -------------------------------------------------------------------------------- /includes/events/system.php: -------------------------------------------------------------------------------- 1 | (new DateTime('now'))->format('Y-m-d H:i:s'), 21 | 'time' => sprintf('%.2fms', (microtime(true) - START_TIME) * 1000), 22 | ], 'events'); 23 | }); 24 | 25 | Event::listen(\MAKS\Velox\Backend\Auth::ON_REGISTER, function ($user) { 26 | App::log('A new user with username "{username}" has registered', [ 27 | 'username' => $user->getUsername() 28 | ], 'events'); 29 | }); 30 | 31 | Event::listen(\MAKS\Velox\Backend\Config::ON_LOAD, function (&$config) { 32 | App::log('The config was loaded', null, 'events'); 33 | 34 | Config::set('eventExecuted', true); 35 | if ($config['eventExecuted']) { 36 | App::log('The config was manipulated', null, 'events'); 37 | } 38 | }); 39 | 40 | Event::listen(\MAKS\Velox\Backend\Controller::ON_CONSTRUCT, function () { 41 | /** @var \MAKS\Velox\Backend\Controller $this */ 42 | $this->vars['__uid'] = uniqid(); 43 | 44 | App::log('The "{class}" has been constructed', ['class' => get_class($this)], 'events'); 45 | }); 46 | 47 | Event::listen(\MAKS\Velox\Backend\Router::ON_REGISTER_HANDLER, function (&$route) { 48 | App::log('The handler for the route "{route}" has been registered', ['route' => $route['expression']], 'events'); 49 | }); 50 | 51 | Event::listen(\MAKS\Velox\Frontend\Data::ON_LOAD, function (&$data) { 52 | App::log('The data was loaded', null, 'events'); 53 | 54 | Data::set('eventExecuted', true); 55 | if ($data['eventExecuted']) { 56 | App::log('The data was manipulated', null, 'events'); 57 | } 58 | }); 59 | 60 | Event::listen(\MAKS\Velox\Frontend\View::BEFORE_RENDER, function (&$variables) { 61 | $variables['__uid'] = uniqid(); 62 | App::log('The UID "{uid}" was added to the view as "$__uid"', ['uid' => $variables['__uid']], 'events'); 63 | }); 64 | 65 | 66 | 67 | // Available events 68 | // ---------------- 69 | // * App::ON_TERMINATE 70 | // * App::ON_SHUTDOWN 71 | // * Auth::ON_REGISTER 72 | // * Auth::AFTER_REGISTER 73 | // * Auth::ON_UNREGISTER 74 | // * Auth::ON_LOGIN 75 | // * Auth::ON_LOGOUT 76 | // * Config::ON_LOAD 77 | // * Config::ON_CACHE 78 | // * Config::ON_CLEAR_CACHE 79 | // * Controller::ON_CONSTRUCT 80 | // * Router::ON_REGISTER_HANDLER 81 | // * Router::ON_REGISTER_MIDDLEWARE 82 | // * Router::ON_START 83 | // * Router::BEFORE_REDIRECT 84 | // * Router::BEFORE_FORWARD 85 | // * Data::ON_LOAD 86 | // * View::BEFORE_RENDER 87 | // * View::ON_CACHE 88 | // * View::ON_CACHE_CLEAR 89 | -------------------------------------------------------------------------------- /bin/app-mirror: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | delete($path . '/' . $file); 45 | } 46 | } 47 | 48 | return rmdir($path); 49 | } 50 | 51 | if (file_exists($path)) { 52 | return unlink($path); 53 | } 54 | 55 | return false; 56 | } 57 | 58 | public function copy(string $source, string $destination): bool 59 | { 60 | if (file_exists($destination)) { 61 | $this->delete($destination); 62 | } 63 | 64 | if (is_dir($source)) { 65 | mkdir($destination, 755, true); 66 | $files = scandir($source); 67 | foreach ($files as $file) { 68 | if ($file !== '.' && $file !== '..') { 69 | $this->copy($source . '/' . $file, $destination . '/' . $file); 70 | } 71 | } 72 | 73 | return true; 74 | } 75 | 76 | if (file_exists($source)) { 77 | return copy($source, $destination); 78 | } 79 | 80 | return false; 81 | } 82 | 83 | public function getValidName($name, string $original): string 84 | { 85 | return !is_int($name) 86 | ? Path::resolve('/public/' . (string)$name) 87 | : str_replace( 88 | Config::get('global.paths.root'), 89 | Config::get('global.paths.public'), 90 | $original 91 | ); 92 | } 93 | }; 94 | 95 | 96 | $link = Config::get('cli.app-mirror.args.link', []); 97 | $copy = Config::get('cli.app-mirror.args.copy', []); 98 | 99 | 100 | foreach ($link as $name => $target) { 101 | $fileSystem->link( 102 | $target, 103 | $fileSystem->getValidName($name, $target) 104 | ); 105 | } 106 | 107 | foreach ($copy as $name => $source) { 108 | $fileSystem->copy( 109 | $source, 110 | $fileSystem->getValidName($name, $source) 111 | ); 112 | } 113 | 114 | 115 | echo 'OK!', PHP_EOL; 116 | exit(0); 117 | -------------------------------------------------------------------------------- /themes/velox/pages/persons/show.phtml: -------------------------------------------------------------------------------- 1 | {! @extends 'velox/pages/persons/index' !} 2 | 3 | 4 | {! @block navigation !} 5 | {# remove navigation by emptying the block #} 6 | {! @endblock !} 7 | 8 | {! @block content !} 9 |
    10 |
    11 |

    Showing: {{ $person->name }}

    12 |
    13 |
    14 |

    {{ $person->name }}

    15 |
    16 |
    17 |
    18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 |
    Id{{ $person->id }}
    Name{{ $person->name }}
    Age{{ $person->age }}
    Username{{ $person->username }}
    E-Mail{{ $person->email }}
    Address{{ $person->address }}
    Company{{ $person->company }}
    48 |
    49 |
    50 |
    51 | 54 | 57 | 62 |
    63 |
    64 |
    65 |
    66 |
    67 |

    Others

    68 | {# parent block loops over an array of persons #} 69 | {! $persons = $others !} 70 | {! @super !} 71 | {! @endblock !} 72 | -------------------------------------------------------------------------------- /classes/Frontend/Data.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright Marwan Al-Soltany 2021 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace MAKS\Velox\Frontend; 13 | 14 | use MAKS\Velox\Backend\Event; 15 | use MAKS\Velox\Backend\Config; 16 | use MAKS\Velox\Helper\Misc; 17 | 18 | /** 19 | * A class that serves as an abstracted data bag/store that is accessible via dot-notation. 20 | * 21 | * Example: 22 | * ``` 23 | * // get all data 24 | * $allData = Data::getAll(); 25 | * 26 | * // check for variable availability 27 | * $someVarExists = Data::has('someVar'); 28 | * 29 | * // get a specific variable or fall back to a default value 30 | * $someVar = Data::get('someVar', 'fallbackValue'); 31 | * 32 | * // set a specific variable value 33 | * Data::set('someNewVar.someKey', 'someValue'); 34 | * ``` 35 | * 36 | * @package Velox\Frontend 37 | * @since 1.0.0 38 | * @api 39 | */ 40 | class Data 41 | { 42 | /** 43 | * This event will be dispatched when the data is loaded. 44 | * This event will be passed a reference to the data array. 45 | * 46 | * @var string 47 | */ 48 | public const ON_LOAD = 'data.on.load'; 49 | 50 | 51 | /** 52 | * The currently loaded data. 53 | */ 54 | protected static array $bag = []; 55 | 56 | 57 | /** 58 | * Loads the data from system configuration. 59 | * 60 | * @return void 61 | */ 62 | protected static function load(): void 63 | { 64 | if (empty(static::$bag)) { 65 | static::$bag = (array)Config::get('data', static::$bag); 66 | 67 | // make `Config::$config['data']` points to `Data::$bag` (the same array in memory) 68 | $config = &Config::getReference(); 69 | $config['data'] = &static::$bag; 70 | 71 | Event::dispatch(self::ON_LOAD, [&static::$bag]); 72 | } 73 | } 74 | 75 | /** 76 | * Checks whether a value of a key exists in `self::$bag` via dot-notation. 77 | * 78 | * @param string $key The dotted key representation. 79 | * 80 | * @return bool 81 | */ 82 | public static function has(string $key): bool 83 | { 84 | static::load(); 85 | 86 | $value = Misc::getArrayValueByKey(self::$bag, $key, null); 87 | 88 | return isset($value); 89 | } 90 | 91 | /** 92 | * Gets a value of a key from `self::$bag` via dot-notation. 93 | * 94 | * @param string $key The dotted key representation. 95 | * @param mixed $default [optional] The default fallback value. 96 | * 97 | * @return mixed The requested value or null. 98 | */ 99 | public static function get(string $key, $default = null) 100 | { 101 | static::load(); 102 | 103 | return Misc::getArrayValueByKey(self::$bag, $key, $default); 104 | } 105 | 106 | /** 107 | * Sets a value of a key in `self::$bag` via dot-notation. 108 | * 109 | * @param string $key The dotted key representation. 110 | * @param mixed $value The value to set. 111 | * 112 | * @return void 113 | */ 114 | public static function set(string $key, $value): void 115 | { 116 | static::load(); 117 | 118 | Misc::setArrayValueByKey(self::$bag, $key, $value); 119 | } 120 | 121 | /** 122 | * Returns the currently loaded data. 123 | * 124 | * @return array 125 | */ 126 | public static function getAll(): ?array 127 | { 128 | static::load(); 129 | 130 | return static::$bag; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /themes/velox/pages/persons/edit.phtml: -------------------------------------------------------------------------------- 1 | {! @extends 'velox/pages/persons/index' !} 2 | 3 | 4 | {! @block navigation !} 5 | {# remove navigation by emptying the block #} 6 | {! @endblock !} 7 | 8 | {! @block content !} 9 |
    10 |
    11 |

    Editing: {{ $person->name }}

    12 |
    13 | 14 | 15 |
    16 | 17 |
    18 | 19 |
    20 |
    21 |
    22 | 23 |
    24 | 25 |
    26 |
    27 |
    28 | 29 |
    30 | 31 |
    32 |
    33 |
    34 | 35 |
    36 | 37 |
    38 |
    39 |
    40 | 41 |
    42 | 43 |
    44 |
    45 |
    46 | 47 |
    48 | 49 |
    50 |
    51 |
    52 |
    53 |
    54 |
    55 |
    56 | 57 |
    58 |
    59 | Cancel 60 |
    61 |
    62 |
    63 |
    64 |
    65 | 66 | {{{ Session::csrf() }}} 67 | 68 |
    69 |
    70 |
    71 |
    72 |
    73 |
    74 | {! @endblock !} 75 | -------------------------------------------------------------------------------- /themes/velox/partials/form.phtml: -------------------------------------------------------------------------------- 1 | open('section', ['class' => 'section is-small']) 4 | ->open('div', ['class' => 'container']) 5 | ->open('div', ['class' => 'columns']) 6 | ->open('div', ['class' => 'column is-two-thirds']) 7 | ->comment('SIMPLE FORM') 8 | ->open('form', ['method' => 'POST', 'class' => 'box']) 9 | ->node(Session::csrf()->html()) 10 | ->open('div', ['class' => 'field']) 11 | ->label('Message', ['class' => 'label']) 12 | ->open('div', ['class' => 'control has-icons-right']) 13 | ->input(null, [ 14 | 'class' => 'input is-large is-success', 15 | 'type' => 'text', 16 | 'name' =>'message', 17 | 'placeholder' => 'Your message ...', 18 | 'required' 19 | ]) 20 | ->open('span', ['class' => 'icon is-small is-right']) 21 | ->i('', ['class' => 'fas fa-check']) 22 | ->close() 23 | ->close() 24 | ->p(!$message ? 'Write anything ...' : 'Write again ...', ['class' => 'help is-success']) 25 | ->close() 26 | ->open('div', ['class' => 'field is-grouped']) 27 | ->open('div', ['class' => 'control']) 28 | ->input(null, [ 29 | 'class' => 'button is-success', 30 | 'type' => 'submit', 31 | 'value' => 'Submit' 32 | ]) 33 | ->close() 34 | ->open('div', ['class' => 'control']) 35 | ->input(null, [ 36 | 'class' => 'button is-success is-light', 37 | 'type' => 'reset', 38 | 'value' => 'Reset' 39 | ]) 40 | ->close() 41 | ->condition($message) 42 | ->open('div', ['class' => 'control']) 43 | ->a('Reload', [ 44 | 'class' => 'button is-danger is-light', 45 | 'href' => 'javascript:window.location.reload();' 46 | ]) 47 | ->close() 48 | ->close() 49 | ->close() 50 | ->close() 51 | ->open('div', ['class' => 'column']) 52 | ->condition($message) 53 | ->open('div', ['class' => 'card']) 54 | ->open('div', ['class' => 'card-content']) 55 | ->p('You wrote:', ['class' => 'subtitle']) 56 | ->p(sprintf('“%s”', $message), ['class' => 'title']) 57 | ->close() 58 | ->open('div', ['class' => 'card-footer']) 59 | ->p('RESULT', ['class' => 'card-footer-item']) 60 | ->close() 61 | ->close() 62 | ->close() 63 | ->close() 64 | ->close() 65 | ->close() 66 | ->echo(); 67 | ?> 68 | -------------------------------------------------------------------------------- /themes/velox/partials/includes/navigation.phtml: -------------------------------------------------------------------------------- 1 | 73 | -------------------------------------------------------------------------------- /includes/routes/web.php: -------------------------------------------------------------------------------- 1 | 'Home']); 16 | }); 17 | 18 | 19 | 20 | Router::middleware('/contact', function () { 21 | $message = Globals::getPost('message'); 22 | if ($message) { 23 | return htmlspecialchars($message, ENT_QUOTES); 24 | } 25 | }, 'POST'); 26 | 27 | Router::handle('/contact', function ($path, $match, $previous) { 28 | return View::render('contact', [ 29 | 'title' => 'Contact', 30 | 'message' => $previous ?? null 31 | ]); 32 | }, ['GET', 'POST']); 33 | 34 | 35 | 36 | $controller = new \App\Controller\DefaultController(); 37 | Router::handle('/example', [$controller, 'exampleAction']); 38 | 39 | // the routes in the following controllers will be registered automatically 40 | // as it's using annotation to define routes, they just needs to be 41 | // instantiated once for the auto-registration to take effect. 42 | new \App\Controller\UsersController(); 43 | new \App\Controller\PersonsController(); 44 | 45 | 46 | 47 | Router::handle('/redirect', function () { 48 | // this will take place in client's browser (client knows about it). 49 | Router::redirect('/'); 50 | }); 51 | 52 | Router::handle('/forward', function () { 53 | // this will take place in the application (client does not know about it). 54 | Router::forward('/'); 55 | }); 56 | 57 | 58 | 59 | // this will match anything after the slash 60 | Router::any('/dump/{var?}', function ($path, $match, $previous) { 61 | return View::render('dump', ['vars' => compact('path', 'match', 'previous')], 'simple'); 62 | }); 63 | // this will match numbers only 64 | Router::get('/number/([0-9]+)', function ($path, $match) { 65 | Router::forward('/dump/' . $match); 66 | }); 67 | // this will match letters only 68 | Router::get('/string/([a-z]+)', function ($path, $match) { 69 | Router::forward('/dump/' . $match); 70 | }); 71 | 72 | 73 | 74 | // The /development-exception and /production-exception routes are for demonstration purposes only. 75 | 76 | Router::handle('/development-exception', function () { 77 | Config::set('global.env', 'DEVELOPMENT'); 78 | 79 | throw new \Exception('Test!'); 80 | }); 81 | 82 | Router::handle('/production-exception', function () { 83 | Config::set('global.env', 'PRODUCTION'); 84 | Config::set('global.errorPages.500', null); // skip configured 500 error page 85 | 86 | throw new \Exception('Test!'); 87 | }); 88 | 89 | 90 | 91 | // The /401, /403, /404, /405, and /500 routes are for demonstration purposes only. 92 | 93 | Router::get('/401', function () { 94 | Auth::fail(); 95 | }); 96 | 97 | Router::get('/403', function () { 98 | // mimicking a 403 error, 99 | // this will render "{global.errorPages.403}" from config 100 | Config::set('session.csrf.methods', ['GET']); // making CSRF checking for GET requests only 101 | 102 | Session::csrf()->token(); // generate a new CSRF token 103 | Session::csrf()->check(); // check the CSRF token 104 | }); 105 | 106 | Router::get('/404-' . uniqid(), function () { 107 | // if requested path is not 404-XXXXXXXXXXXXX, 108 | // request will be forwarded to Router::handleRouteNotFound() 109 | // or fall back to render "{global.errorPages.404}" 110 | return ''; 111 | }); 112 | 113 | Router::post('/405', function () { 114 | // if request method is not POST, 115 | // request will be forwarded to Router::handleMethodNotAllowed() 116 | // or fall back to render "{global.errorPages.403}" from config 117 | return ''; 118 | }); 119 | 120 | Router::get('/500', function () { 121 | // mimicking a 500 error, 122 | // this will render "{global.errorPages.500}" from config 123 | Config::set('global.env', 'PRODUCTION'); 124 | 125 | throw new \Exception('Test!'); 126 | }); 127 | 128 | 129 | 130 | // registers all pages configured in "./config/data.php" 131 | foreach ((array)Config::get('data.pages') as $page) { 132 | Router::handle($page['route'], function () use ($page) { 133 | return View::render($page['page'], $page['variables'], $page['layout']); 134 | }, $page['method']); 135 | } 136 | -------------------------------------------------------------------------------- /app/Controller/UsersController.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright Marwan Al-Soltany 2021 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Controller; 13 | 14 | use MAKS\Velox\Backend\Controller; 15 | 16 | class UsersController extends Controller 17 | { 18 | /** 19 | * {@inheritDoc} 20 | */ 21 | protected function associateModel(): ?string 22 | { 23 | return $this->config->get('auth.user.model'); 24 | } 25 | 26 | /** 27 | * {@inheritDoc} 28 | */ 29 | protected function registerRoutes(): bool 30 | { 31 | return true; 32 | } 33 | 34 | /** 35 | * @route("/register", {GET, POST}) 36 | * 37 | * @return string|void 38 | */ 39 | public function registerAction() 40 | { 41 | if ($this->auth->check()) { 42 | return $this->router->forward('/auth'); 43 | } 44 | 45 | if ($this->globals->server->get('REQUEST_METHOD') == 'GET') { 46 | return $this->view->render('users/register', [ 47 | 'title' => 'Register', 48 | ]); 49 | } 50 | 51 | $username = $this->globals->post->get('username'); 52 | $password = $this->globals->post->get('password'); 53 | 54 | $usernameIsValid = preg_match('/[a-zA-Z0-9.-_]+/', $username); 55 | 56 | if (!$usernameIsValid) { 57 | return $this->view->render('users/register', [ 58 | 'title' => 'Register', 59 | ]); 60 | } 61 | 62 | $success = $this->auth->register($username, $password); 63 | 64 | if (!$success) { 65 | $this->session->flash('Username already taken!', 'notification', true); 66 | 67 | return $this->view->render('users/register', [ 68 | 'title' => 'Register', 69 | ]); 70 | } 71 | 72 | $this->session->flash('User was registered successfully!', 'notification'); 73 | 74 | return $this->router->redirect('/login'); 75 | } 76 | 77 | /** 78 | * @route("/unregister", {GET}) 79 | * 80 | * @return void 81 | */ 82 | public function unregisterAction() 83 | { 84 | if ($this->auth->check() === false) { 85 | $this->auth->fail(); 86 | } 87 | 88 | $this->auth->unregister( 89 | $this->auth->user()->getUsername() 90 | ); 91 | 92 | $this->session->flash('User was unregistered successfully!', 'notification'); 93 | 94 | return $this->router->redirect('/register'); 95 | } 96 | 97 | /** 98 | * @route("/login", {GET, POST}) 99 | * 100 | * @return string|void 101 | */ 102 | public function loginAction() 103 | { 104 | if ($this->auth->check()) { 105 | return $this->router->forward('/auth'); 106 | } 107 | 108 | if ($this->globals->server->get('REQUEST_METHOD') == 'GET') { 109 | return $this->view->render('users/login', [ 110 | 'title' => 'Log in', 111 | ]); 112 | } 113 | 114 | $username = $this->globals->post->get('username'); 115 | $password = $this->globals->post->get('password'); 116 | 117 | $success = $this->auth->login($username, $password); 118 | 119 | if ($success) { 120 | return $this->router->redirect('/auth'); 121 | } else { 122 | $this->session->flash('Wrong username or password!', 'notification', true); 123 | 124 | return $this->view->render('users/login', [ 125 | 'title' => 'Log in', 126 | ]); 127 | } 128 | 129 | return $this->router->redirect('/login'); 130 | } 131 | 132 | /** 133 | * @route("/logout", {GET}) 134 | * 135 | * @return void 136 | */ 137 | public function logoutAction() 138 | { 139 | $this->auth->logout(); 140 | 141 | return $this->router->redirect('/login'); 142 | } 143 | 144 | /** 145 | * @route("/auth", {GET}) 146 | * 147 | * @return void 148 | */ 149 | public function indexAction() 150 | { 151 | return $this->view->render('users/index', [ 152 | 'title' => 'Auth', 153 | 'user' => $this->auth->user(), 154 | ]); 155 | } 156 | 157 | /** 158 | * @route("/auth*", {GET}) 159 | * 160 | * @return void 161 | */ 162 | public function authMiddleware() 163 | { 164 | if ($this->auth->check() === false) { 165 | $this->auth->fail(); 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /classes/Backend/Session/CSRF.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright Marwan Al-Soltany 2021 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace MAKS\Velox\Backend\Session; 13 | 14 | use MAKS\Velox\App; 15 | use MAKS\Velox\Backend\Config; 16 | use MAKS\Velox\Backend\Globals; 17 | use MAKS\Velox\Backend\Session; 18 | use MAKS\Velox\Frontend\HTML; 19 | 20 | /** 21 | * A class that offers a simple interface to protect against Cross-Site Request Forgery. 22 | * 23 | * Example: 24 | * ``` 25 | * // create, store, and return a token 26 | * $csrf = new CSRF(); 27 | * $token = $csrf->token(); 28 | * 29 | * // render a hidden input field containing the token 30 | * $html = $csrf->html(); 31 | * 32 | * // check if the token is valid 33 | * $csrf->check(); 34 | * 35 | * // validate if request token matches the stored token 36 | * $status = $csrf->isValid(); 37 | * ``` 38 | * 39 | * @package Velox\Backend\Session 40 | * @since 1.5.4 41 | */ 42 | class CSRF 43 | { 44 | private string $name; 45 | 46 | private string $token; 47 | 48 | 49 | /** 50 | * Class constructor. 51 | * 52 | * @param string|null $name The name of the CSRF token (input field). 53 | */ 54 | public function __construct(?string $name = null) 55 | { 56 | Session::start(); 57 | 58 | $this->name = $name ?? Config::get('session.csrf.name', '_token'); 59 | $this->token = Session::get($this->name) ?? ''; 60 | } 61 | 62 | /** 63 | * Returns the HTML input element containing the CSRF token. 64 | */ 65 | public function __toString() 66 | { 67 | return $this->html(); 68 | } 69 | 70 | 71 | /** 72 | * Returns an HTML input element containing a CSRF token after storing it in the session. 73 | * This method will be called automatically if the object is casted to a string. 74 | * 75 | * @return string 76 | */ 77 | public function html(): string 78 | { 79 | return HTML::input(null, [ 80 | 'type' => 'hidden', 81 | 'name' => $this->name, 82 | 'value' => $this->token() 83 | ]); 84 | } 85 | 86 | /** 87 | * Generates a CSRF token, stores it in the session and returns it. 88 | * 89 | * @return string The CSRF token. 90 | */ 91 | public function token(): string 92 | { 93 | $this->token = empty($this->token) ? bin2hex(random_bytes(64)) : $this->token; 94 | 95 | Session::set($this->name, $this->token); 96 | 97 | return $this->token; 98 | } 99 | 100 | /** 101 | * Checks whether the request token matches the token stored in the session. 102 | * 103 | * @return bool 104 | */ 105 | public function check(): void 106 | { 107 | if ($this->isValid()) { 108 | return; 109 | } 110 | 111 | $this->fail(); 112 | } 113 | 114 | /** 115 | * Renders 403 error page. 116 | * 117 | * @return void 118 | * 119 | * @codeCoverageIgnore Can't test methods that send headers. 120 | */ 121 | public static function fail(): void 122 | { 123 | App::log('Responded with 403 to the request for "{uri}". CSRF is detected. Client IP address {ip}', [ 124 | 'uri' => Globals::getServer('REQUEST_URI'), 125 | 'ip' => Globals::getServer('REMOTE_ADDR'), 126 | ], 'system'); 127 | 128 | App::abort(403, null, 'Invalid CSRF token!'); 129 | } 130 | 131 | /** 132 | * Validate the request token with the token stored in the session. 133 | * 134 | * @return bool Whether the request token matches the stored one or not. 135 | */ 136 | public function isValid(): bool 137 | { 138 | if ($this->isWhitelisted() || $this->isIdentical()) { 139 | return true; 140 | } 141 | 142 | Session::cut($this->name); 143 | 144 | return false; 145 | } 146 | 147 | private function isWhitelisted(): bool 148 | { 149 | $method = Globals::getServer('REQUEST_METHOD'); 150 | $client = Globals::getServer('REMOTE_HOST') ?? Globals::getServer('REMOTE_ADDR'); 151 | 152 | return ( 153 | in_array($client, Config::get('session.csrf.whitelisted', [])) || 154 | !in_array($method, Config::get('session.csrf.methods', [])) 155 | ); 156 | } 157 | 158 | private function isIdentical(): bool 159 | { 160 | $token = Globals::cutPost($this->name) ?? Globals::cutGet($this->name) ?? ''; 161 | 162 | return empty($this->token) || hash_equals($this->token, $token); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /classes/Backend/Session/Flash.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright Marwan Al-Soltany 2021 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace MAKS\Velox\Backend\Session; 13 | 14 | use MAKS\Velox\Backend\Globals; 15 | use MAKS\Velox\Frontend\HTML; 16 | use MAKS\Velox\Helper\Misc; 17 | 18 | /** 19 | * A class that offers a simple interface to write flash messages. 20 | * 21 | * Example: 22 | * ``` 23 | * // write a flash message 24 | * $flash = new Flash(); 25 | * $flash->message('Some message!'); 26 | * 27 | * // write a flash message that will be rendered immediately 28 | * $flash->message('Some other message!', 'success', true); 29 | * 30 | * // write a flash message with a predefined type 31 | * $flash->error('Some error!', true); 32 | * 33 | * // write a flash message with your own type 34 | * $flash->yourCustomType('Custom type!'); 35 | * 36 | * // render the flash messages 37 | * $flash->render(); 38 | * 39 | * // render the flash messages using a custom callback 40 | * $flash->render(function ($text, $type) { 41 | * return sprintf('
    %s
    ', $type, $text); 42 | * }); 43 | * ``` 44 | * 45 | * @package Velox\Backend\Session 46 | * @since 1.5.4 47 | * 48 | * @method static success(string $text, bool $now = false) Adds a `success` message to the flash. 49 | * @method static info(string $text, bool $now = false) Adds an `info` message to the flash. 50 | * @method static warning(string $text, bool $now = false) Adds a `warning` message to the flash. 51 | * @method static error(string $text, bool $now = false) Adds an `error` message to the flash. 52 | * @method static anyType(string $text, bool $now = false) Adds a message of `anyType` to the flash. `anyType` is user defined and will be used as a CSS class if the default rendering callback is used (`camelCase` will be changed to `kebab-case` automatically). 53 | */ 54 | class Flash 55 | { 56 | private string $name; 57 | 58 | private array $messages; 59 | 60 | 61 | /** 62 | * Class constructor. 63 | * 64 | * @param string|null $name The name of the flash messages store (session key). 65 | */ 66 | public function __construct(?string $name = null) 67 | { 68 | $this->name = $name ?? '_flash'; 69 | $this->messages = Globals::getSession($this->name) ?? []; 70 | 71 | Globals::setSession($this->name, []); 72 | } 73 | 74 | /** 75 | * Makes the class callable as a function, the call is forwarded to `self::message()`. 76 | */ 77 | public function __invoke() 78 | { 79 | return $this->message(...func_get_args()); 80 | } 81 | 82 | /** 83 | * Aliases some magic methods for `self::message()`. 84 | */ 85 | public function __call(string $method, array $arguments) 86 | { 87 | return $this->message( 88 | Misc::transform($method, 'kebab'), 89 | $arguments[0] ?? '', 90 | $arguments[1] ?? false 91 | ); 92 | } 93 | 94 | /** 95 | * Returns the HTML containing the flash messages. 96 | */ 97 | public function __toString() 98 | { 99 | return $this->render(); 100 | } 101 | 102 | 103 | /** 104 | * Writes a flash message to the session. 105 | * 106 | * @param string $type Message type. 107 | * @param string $text Message text. 108 | * @param bool $now [optional] Whether to write and make the message available for rendering immediately or wait for the next request. 109 | * 110 | * @return $this 111 | */ 112 | public function message(string $type, string $text, bool $now = false) 113 | { 114 | $id = uniqid(md5($text) . '-'); 115 | 116 | if ($now) { 117 | $this->messages[$id] = [ 118 | 'type' => $type, 119 | 'text' => $text 120 | ]; 121 | 122 | return $this; 123 | } 124 | 125 | Globals::setSession($this->name . '.' . $id, [ 126 | 'type' => $type, 127 | 'text' => $text 128 | ]); 129 | 130 | return $this; 131 | } 132 | 133 | /** 134 | * Renders the flash messages using the default callback or the passed one. 135 | * This method will be called automatically if the object is casted to a string. 136 | * 137 | * @param callable|null $callback Rendering callback. The callback will be passed: `$text`, `$type` 138 | * 139 | * @return string The result of the passed callback or the default one. 140 | */ 141 | public function render(?callable $callback = null): string 142 | { 143 | $callback = $callback ?? function ($text, $type) { 144 | return HTML::div($text, [ 145 | 'class' => 'flash-message ' . $type 146 | ]); 147 | }; 148 | 149 | $html = ''; 150 | 151 | foreach ($this->messages as $message) { 152 | $html .= $callback($message['text'], $message['type']); 153 | } 154 | 155 | return $html; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /classes/Frontend/View/Compiler.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright Marwan Al-Soltany 2021 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace MAKS\Velox\Frontend\View; 13 | 14 | use MAKS\Velox\Backend\Config; 15 | use MAKS\Velox\Frontend\Path; 16 | use MAKS\Velox\Frontend\View\Engine; 17 | 18 | /** 19 | * A class that offers some utility functions to require, parse, and compile view files. 20 | * 21 | * @package Velox\Frontend\View 22 | * @since 1.5.4 23 | */ 24 | class Compiler 25 | { 26 | /** 27 | * Compiles a PHP file with the passed variables. 28 | * 29 | * @param string $file An absolute path to the file that should be compiled. 30 | * @param string $type The type of the file (just a name to make for friendly exceptions). 31 | * @param array|null [optional] An associative array of the variables to pass. 32 | * 33 | * @return string 34 | * 35 | * @throws \Exception If failed to compile the file. 36 | */ 37 | public static function compile(string $file, string $type, ?array $variables = null): string 38 | { 39 | ob_start(); 40 | 41 | try { 42 | self::require($file, $variables); 43 | } catch (\Exception $error) { 44 | // clean started buffer before throwing the exception 45 | ob_end_clean(); 46 | 47 | throw $error; 48 | } 49 | 50 | $buffer = ob_get_contents(); 51 | ob_end_clean(); 52 | 53 | if ($buffer === false) { 54 | $name = basename($file, Config::get('view.fileExtension')); 55 | throw new \Exception("Something went wrong when trying to compile the {$type} with the name '{$name}' in {$file}"); 56 | } 57 | 58 | return trim($buffer); 59 | } 60 | 61 | /** 62 | * Requires a PHP file and pass it the passed variables. 63 | * 64 | * @param string $file An absolute path to the file that should be compiled. 65 | * @param array|null $variables [optional] An associative array of the variables to pass. 66 | * 67 | * @return void 68 | * 69 | * @throws \Exception If the file could not be loaded. 70 | */ 71 | public static function require(string $file, ?array $variables = null): void 72 | { 73 | $file = self::resolve($file); 74 | 75 | if (!file_exists($file)) { 76 | throw new \Exception( 77 | "Could not load the file with the path '{$file}' nor fall back to a parent. Check if the file exists" 78 | ); 79 | } 80 | 81 | $_file = static::parse($file); 82 | 83 | unset($file); 84 | 85 | if ($variables !== null) { 86 | extract($variables, EXTR_OVERWRITE); 87 | unset($variables); 88 | } 89 | 90 | require($_file); 91 | 92 | unset($_file); 93 | } 94 | 95 | /** 96 | * Parses a file through the templating engine and returns a path to the compiled file. 97 | * 98 | * @param string $file The file to parse. 99 | * 100 | * @return string 101 | */ 102 | public static function parse(string $file): string 103 | { 104 | if (!Config::get('view.engine.enabled', true)) { 105 | return $file; 106 | } 107 | 108 | static $engine = null; 109 | 110 | if ($engine === null) { 111 | $engine = new Engine( 112 | (string)Config::get('global.paths.themes') . '/', 113 | (string)Config::get('view.fileExtension', '.phtml'), 114 | (string)Config::get('global.paths.storage') . '/temp/views/', 115 | (bool)Config::get('view.engine.cache', true), 116 | (bool)Config::get('view.engine.debug', false) 117 | ); 118 | } 119 | 120 | $file = $engine->getCompiledFile(strtr($file, [ 121 | Path::normalize(Config::get('global.paths.themes'), '') => '' 122 | ])); 123 | 124 | return $file; 125 | } 126 | 127 | /** 128 | * Resolves a file from the active theme or inherits it from a parent theme. 129 | * 130 | * @param string $file 131 | * 132 | * @return string 133 | */ 134 | public static function resolve(string $file): string 135 | { 136 | if (file_exists($file)) { 137 | return $file; 138 | } 139 | 140 | if (Config::get('view.inherit')) { 141 | $active = Config::get('theme.active'); 142 | $parent = Config::get('theme.parent'); 143 | $themes = Config::get('global.paths.themes'); 144 | $nameWrapper = basename($themes) . DIRECTORY_SEPARATOR . '%s'; 145 | 146 | foreach ((array)$parent as $substitute) { 147 | $fallbackFile = strtr($file, [ 148 | sprintf($nameWrapper, $active) => sprintf($nameWrapper, $substitute) 149 | ]); 150 | 151 | if (file_exists($fallbackFile)) { 152 | $file = $fallbackFile; 153 | break; 154 | } 155 | } 156 | } 157 | 158 | return $file; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /themes/velox/pages/persons/index.phtml: -------------------------------------------------------------------------------- 1 | {! Data::set('page.title', $title ?? 'Persons') !} 2 | 3 |
    4 | {{{ View::partial('hero', [ 5 | 'title' => 'Persons', 6 | 'subtitle' => 'CRUD Demo', 7 | 'class' => 'is-small is-primary is-bold' 8 | ]) }}} 9 |
    10 |
    11 |
    12 |
    13 | {! @block flashMessages !} 14 |
    15 | {{{ Session::flash() }}} 16 |
    17 | {! @endblock!} 18 | {! @block(flashMessages) !} 19 | 20 | {! @block navigation !} 21 | 30 |
    31 | {! @endblock!} 32 | {! @block(navigation) !} 33 | 34 | {! @block content !} 35 |
    36 | {! @if (isset($persons) && count($persons)) !} 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | {! @foreach ($persons as $person) !} 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 77 | 78 | {! @endforeach !} 79 | 80 |
    IdNameAgeUsernameE-MailAddressCompanyAction
    {{ $person->id }}{{ $person->name }}{{ $person->age }}{{ $person->username }}{{ $person->email }}{{ $person->address }}{{ $person->company }} 61 |
    62 |
    63 | Show 64 |
    65 |
    66 | Edit 67 |
    68 |
    69 |
    70 | 71 | {{{ Session::csrf() }}} 72 | 73 |
    74 |
    75 |
    76 |
    81 | {! @else !} 82 |

    No persons found.

    83 |

     

    84 |

    To create a new one, click here.

    85 | {! @endif !} 86 |
    87 | {! @endblock !} 88 | {# a block has to be called at least once in order to be rendered #} 89 | {! @block(content) !} 90 |
    91 |
    92 |
    93 |
    94 |
    95 | -------------------------------------------------------------------------------- /classes/Backend/Event.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright Marwan Al-Soltany 2021 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace MAKS\Velox\Backend; 13 | 14 | /** 15 | * A class that offers simple events handling functionality (dispatching and listening). 16 | * 17 | * Example: 18 | * ``` 19 | * // listening on an event 20 | * Event::listen('some.event', function ($arg1, $arg2) { 21 | * // do some stuff ... 22 | * }); 23 | * 24 | * // dispatching an event 25 | * Event::dispatch('some.event', [$arg1, $arg2]); 26 | * 27 | * // check if an event has listeners 28 | * Event::hasListener('some.event'); 29 | * 30 | * // check if an event is dispatched 31 | * Event::isDispatch('some.event'); 32 | * 33 | * // get a registered or a new event object 34 | * Event::get('some.event'); 35 | * 36 | * // get a all registered events 37 | * Event::getRegisteredEvents(); 38 | * ``` 39 | * 40 | * @package Velox\Backend 41 | * @since 1.2.0 42 | * @api 43 | */ 44 | class Event 45 | { 46 | /** 47 | * Here live all bindings. 48 | */ 49 | protected static array $events = []; 50 | 51 | 52 | /** 53 | * Dispatches the passed event by executing all attached listeners and passes them the passed arguments. 54 | * 55 | * @param string $event Event name. 56 | * @param array $arguments [optional] Arguments array. 57 | * Note that the arguments will be spread (`...$args`) on the callback and an additional argument of the event name will be appended to the arguments. 58 | * @param object|null $callbackThis [optional] The object the callback should be bound to. 59 | * 60 | * @return void 61 | */ 62 | public static function dispatch(string $event, ?array $arguments = null, ?object $callbackThis = null): void 63 | { 64 | if (static::isDispatched($event) === false) { 65 | static::get($event)->dispatched = true; 66 | } 67 | 68 | if (static::hasListener($event) === false) { 69 | return; 70 | } 71 | 72 | $callbacks = &static::get($event)->listeners; 73 | 74 | $parameters = array_merge(array_values($arguments ?? []), [$event]); 75 | 76 | // array_walk is used instead of foreach to give the possibility 77 | // for the callback to attach new listeners to the current event 78 | array_walk($callbacks, function (&$callback) use (&$callbackThis, &$parameters) { 79 | /** @var \Closure $callback */ 80 | if ($callbackThis) { 81 | $callback->call($callbackThis, ...$parameters); 82 | 83 | return; 84 | } 85 | 86 | $callback(...$parameters); 87 | }); 88 | } 89 | 90 | /** 91 | * Listens on the passed event and attaches the passed callback to it. 92 | * 93 | * @param string $event Event name. 94 | * @param callable $callback A callback to process the event. 95 | * @param int $priority [optional] The priority of the listener. 96 | * Higher number means higher priority, numbers can be positive only (negative numbers are treated as zero). 97 | * Zero (`0`) is reserved to add to the end of the stack (lowest priority). 98 | * 99 | * @return void 100 | */ 101 | public static function listen(string $event, callable $callback, int $priority = 0): void 102 | { 103 | $callback = $callback instanceof \Closure ? $callback : \Closure::fromCallable($callback); 104 | 105 | $listeners = &static::get($event)->listeners; 106 | 107 | $priority <= 0 108 | ? array_push($listeners, $callback) 109 | : array_splice($listeners, $priority * -1, 0, $callback); 110 | } 111 | 112 | /** 113 | * Checks whether an event has already been dispatched or not. 114 | * 115 | * @param string $event Event name. 116 | * 117 | * @return bool 118 | * 119 | * @since 1.5.0 120 | */ 121 | public static function isDispatched(string $event): bool 122 | { 123 | return static::get($event)->dispatched === true; 124 | } 125 | 126 | /** 127 | * Checks whether an event has any listeners or not. 128 | * 129 | * @param string $event Event name. 130 | * 131 | * @return bool 132 | * 133 | * @since 1.5.0 134 | */ 135 | public static function hasListener(string $event): bool 136 | { 137 | return empty(static::get($event)->listeners) === false; 138 | } 139 | 140 | /** 141 | * Returns an event object by its name or creates it if it does not exist. 142 | * The event object consists of the following properties: 143 | * - `name`: The event name. 144 | * - `dispatched`: A boolean flag indicating whether the event has been dispatched or not. 145 | * - `listeners`: An array of callbacks. 146 | * 147 | * @param string $event Event name. 148 | * 149 | * @return object 150 | * 151 | * @since 1.5.0 152 | */ 153 | public static function get(string $event): object 154 | { 155 | return static::$events[$event] ?? static::create($event); 156 | } 157 | 158 | /** 159 | * Returns array of all registered events as an array `['event.name' => $eventObject, ...]`. 160 | * 161 | * @return object[] 162 | */ 163 | public static function getRegisteredEvents(): array 164 | { 165 | return static::$events; 166 | } 167 | 168 | /** 169 | * Creates an event object and adds it to the registered events. 170 | * 171 | * @param string $event Event name. 172 | * 173 | * @return object 174 | * 175 | * @since 1.5.0 176 | */ 177 | protected static function create(string $event): object 178 | { 179 | return static::$events[$event] = (object)[ 180 | 'name' => $event, 181 | 'dispatched' => false, 182 | 'listeners' => [], 183 | ]; 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /classes/Backend/Model/Element.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright Marwan Al-Soltany 2021 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace MAKS\Velox\Backend\Model; 13 | 14 | use MAKS\Velox\Backend\Exception; 15 | use MAKS\Velox\Backend\Model\DBAL; 16 | use MAKS\Velox\Helper\Misc; 17 | 18 | /** 19 | * An abstract class that holds the base functionality of a model. 20 | * NOTE: This class is not meant to be used directly. 21 | * 22 | * @package Velox\Backend\Model 23 | * @since 1.5.1 24 | */ 25 | abstract class Element extends DBAL implements \ArrayAccess, \Traversable, \IteratorAggregate 26 | { 27 | /** 28 | * Model attributes. Corresponds to table columns. 29 | */ 30 | protected array $attributes; 31 | 32 | 33 | /** 34 | * Class constructor. 35 | * 36 | * Keep all constructor arguments optional when extending the class. 37 | * Or use `self::bootstrap()` instead. 38 | * 39 | * @param array $attributes [optional] The attributes to set on the model. 40 | */ 41 | public function __construct(?array $attributes = []) 42 | { 43 | $this->attributes = array_merge(array_fill_keys($this->getColumns(), null), $attributes ?? []); 44 | 45 | if ($this->isMigrated() === false) { 46 | $this->migrate(); 47 | } 48 | 49 | $this->bootstrap(); 50 | } 51 | 52 | /** 53 | * Override this method to add your own bootstrap code to the modal. 54 | * 55 | * @return void 56 | */ 57 | protected function bootstrap(): void 58 | { 59 | // implemented as needed 60 | } 61 | 62 | /** 63 | * Asserts that the model attribute name is valid. 64 | * 65 | * @param mixed $name The name to validate. 66 | * 67 | * @return void 68 | * 69 | * @throws \OutOfBoundsException If attribute name is not a part of model `$columns`. 70 | */ 71 | protected static function assertAttributeExists($name): void 72 | { 73 | $columns = static::getColumns(); 74 | 75 | if (!in_array((string)$name, $columns)) { 76 | Exception::throw( 77 | 'UnknownAttributeException:OutOfBoundsException', 78 | sprintf('Cannot find attribute with the name "%s". %s model table does not consist of this column', $name, static::class) 79 | ); 80 | } 81 | } 82 | 83 | /** 84 | * Gets the specified model attribute. 85 | * 86 | * @param string $name Attribute name as specified in `$columns`. 87 | * 88 | * @return mixed Attribute value. 89 | * 90 | * @throws \OutOfBoundsException If the attribute does not exists. 91 | */ 92 | public function get(string $name) 93 | { 94 | $this->assertAttributeExists($name); 95 | 96 | return $this->attributes[$name]; 97 | } 98 | 99 | /** 100 | * Sets the specified model attribute. 101 | * 102 | * @param string $name Attribute name as specified in `$columns`. 103 | * @param mixed $value Attribute value. 104 | * 105 | * @return $this 106 | * 107 | * @throws \OutOfBoundsException If the attribute does not exists. 108 | */ 109 | public function set(string $name, $value): self 110 | { 111 | $this->assertAttributeExists($name); 112 | 113 | $this->attributes[$name] = $value; 114 | 115 | return $this; 116 | } 117 | 118 | /** 119 | * Gets all model attributes. 120 | * 121 | * @return array Model attributes. 122 | */ 123 | public function getAttributes(): array 124 | { 125 | return $this->attributes; 126 | } 127 | 128 | /** 129 | * Sets all or a subset of model attributes. 130 | * 131 | * @param array $attributes Model attributes. 132 | * 133 | * @return $this 134 | */ 135 | public function setAttributes(array $attributes): self 136 | { 137 | foreach ($attributes as $key => $value) { 138 | in_array($key, $this->getColumns()) && $this->set($key, $value); 139 | } 140 | 141 | return $this; 142 | } 143 | 144 | 145 | /** 146 | * Makes attributes accessible via public property access notation. 147 | * Examples: `model_id` as `$model->modelId` 148 | */ 149 | public function __get(string $name) 150 | { 151 | $name = Misc::transform($name, 'snake'); 152 | 153 | return $this->get($name); 154 | } 155 | 156 | /** 157 | * Makes attributes accessible via public property assignment notation. 158 | * Examples: `model_id` as `$model->modelId` 159 | */ 160 | public function __set(string $name, $value) 161 | { 162 | $name = Misc::transform($name, 'snake'); 163 | 164 | return $this->set($name, $value); 165 | } 166 | 167 | /** 168 | * Makes attributes consumable via `isset()`. 169 | */ 170 | public function __isset(string $name) 171 | { 172 | $name = Misc::transform($name, 'snake'); 173 | 174 | return $this->get($name) !== null; 175 | } 176 | 177 | /** 178 | * Makes attributes consumable via `unset()`. 179 | */ 180 | public function __unset(string $name) 181 | { 182 | $name = Misc::transform($name, 'snake'); 183 | 184 | return $this->set($name, null); 185 | } 186 | 187 | /** 188 | * Makes the model safely cloneable via the `clone` keyword. 189 | */ 190 | public function __clone() 191 | { 192 | $this->set($this->getPrimaryKey(), null); 193 | } 194 | 195 | /** 196 | * Makes the model safely consumable via `serialize()`. 197 | */ 198 | public function __sleep() 199 | { 200 | return ['attributes']; 201 | } 202 | 203 | 204 | /** 205 | * `ArrayAccess::offsetGet()` interface implementation. 206 | */ 207 | #[\ReturnTypeWillChange] 208 | public function offsetGet($offset) 209 | { 210 | return $this->get($offset); 211 | } 212 | 213 | /** 214 | * `ArrayAccess::offsetSet()` interface implementation. 215 | */ 216 | #[\ReturnTypeWillChange] 217 | public function offsetSet($offset, $value): void 218 | { 219 | $this->set($offset, $value); 220 | } 221 | 222 | /** 223 | * `ArrayAccess::offsetExists()` interface implementation. 224 | */ 225 | #[\ReturnTypeWillChange] 226 | public function offsetExists($offset): bool 227 | { 228 | return $this->get($offset) !== null; 229 | } 230 | 231 | /** 232 | * `ArrayAccess::offsetUnset()` interface implementation. 233 | */ 234 | #[\ReturnTypeWillChange] 235 | public function offsetUnset($offset): void 236 | { 237 | $this->set($offset, null); 238 | } 239 | 240 | 241 | /** 242 | * `IteratorAggregate::getIterator()` interface implementation. 243 | */ 244 | public function getIterator(): \Traversable 245 | { 246 | return new \ArrayIterator($this->attributes); 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /classes/Backend/Session.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright Marwan Al-Soltany 2021 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace MAKS\Velox\Backend; 13 | 14 | use MAKS\Velox\Backend\Session\Flash; 15 | use MAKS\Velox\Backend\Session\CSRF; 16 | use MAKS\Velox\Backend\Config; 17 | 18 | /** 19 | * A class that offers a simple interface to work with sessions. 20 | * 21 | * Example: 22 | * ``` 23 | * // start a session 24 | * Session::start(); 25 | * 26 | * // check for variable availability 27 | * $someVarExists = Session::has('someVar'); 28 | * 29 | * // set a session variable 30 | * Session::set('someVar', $value); 31 | * 32 | * // get a session variable 33 | * $someVar = Session::get('someVar'); 34 | * 35 | * // destroy a session 36 | * Session::destroy(); 37 | * 38 | * // get an instance of the Flash class 39 | * $flash = Session::flash(); 40 | * 41 | * // get an instance of the CSRF class 42 | * $flash = Session::csrf(); 43 | * ``` 44 | * 45 | * @package Velox\Backend\Session 46 | * @since 1.3.0 47 | * @api 48 | */ 49 | final class Session 50 | { 51 | /** 52 | * Class constructor. 53 | * 54 | * @param int|null $expiration Session expiration time in minutes. 55 | * @param string|null $limiter Session limiter. 56 | * @param string|null $path Session save path. 57 | */ 58 | public function __construct(?int $expiration = null, ?string $limiter = null, ?string $path = null) 59 | { 60 | $this->start($expiration, $limiter, $path); 61 | } 62 | 63 | /** 64 | * Starts the session if it is not already started. 65 | * 66 | * @param int|null [optional] $expiration Session expiration time in minutes. 67 | * @param string|null [optional] $limiter Session limiter. 68 | * @param string|null [optional] $path Session save path. 69 | * 70 | * @return bool True if the session was started, false otherwise. 71 | */ 72 | public static function start(?int $expiration = null, ?string $limiter = null, ?string $path = null): bool 73 | { 74 | $path ??= Config::get('session.path', Config::get('global.paths.storage') . '/sessions'); 75 | $limiter ??= Config::get('session.cache.limiter', 'nocache'); 76 | $expiration ??= Config::get('session.cache.expiration', 180); 77 | 78 | file_exists($path) || mkdir($path, 0744, true); 79 | 80 | session_save_path() != $path && session_save_path($path); 81 | session_cache_expire() != $expiration && session_cache_expire($expiration); 82 | session_cache_limiter() != $limiter && session_cache_limiter($limiter); 83 | 84 | $status = session_status() != PHP_SESSION_NONE || session_start(['name' => 'VELOX']); 85 | 86 | return $status; 87 | } 88 | 89 | /** 90 | * Destroys all of the data associated with the current session. 91 | * This method does not unset any of the global variables associated with the session, or unset the session cookie. 92 | * 93 | * @return bool True if the session was destroyed, false otherwise. 94 | */ 95 | public static function destroy(): bool 96 | { 97 | return session_destroy(); 98 | } 99 | 100 | /** 101 | * Unsets the session superglobal 102 | * This method deletes (truncates) only the variables in the session, session still exists. 103 | * 104 | * @return bool True if the session was unset, false otherwise. 105 | */ 106 | public static function unset(): bool 107 | { 108 | return session_unset(); 109 | } 110 | 111 | /** 112 | * Clears the session entirely. 113 | * This method will unset the session, destroy the session, commit (close writing) to the session, and reset the session cookie (new expiration). 114 | * 115 | * @return bool True if the session was cleared, false otherwise. 116 | */ 117 | public static function clear(): bool 118 | { 119 | $name = session_name(); 120 | $cookie = session_get_cookie_params(); 121 | 122 | setcookie($name, '', 0, $cookie['path'], $cookie['domain'], $cookie['secure'], $cookie['httponly'] ?? false); 123 | // not testable in CLI, headers already sent 124 | // @codeCoverageIgnoreStart 125 | $unset = session_unset(); 126 | $destroy = session_destroy(); 127 | $commit = session_commit(); 128 | 129 | return ($unset && $destroy && $commit); 130 | // @codeCoverageIgnoreEnd 131 | } 132 | 133 | /** 134 | * Checks if a value exists in the session. 135 | * 136 | * @param string $key The key to check. Dot-notation can be used with nested arrays. 137 | * 138 | * @return bool True if the key exists, false otherwise. 139 | */ 140 | public static function has(string $key): bool 141 | { 142 | return Globals::getSession($key) !== null; 143 | } 144 | 145 | /** 146 | * Gets a value from the session. 147 | * 148 | * @param string $key The key to get. Dot-notation can be used with nested arrays. 149 | * 150 | * @return mixed The value of the key, or null if the key does not exist. 151 | */ 152 | public static function get(string $key) 153 | { 154 | return Globals::getSession($key); 155 | } 156 | 157 | /** 158 | * Sets a value in the session. 159 | * 160 | * @param string $key The key to set. Dot-notation can be used with nested arrays. 161 | * @param mixed $value The value to set. 162 | * 163 | * @return static The current instance. 164 | */ 165 | public static function set(string $key, $value) 166 | { 167 | Globals::setSession($key, $value); 168 | 169 | return new static(); 170 | } 171 | 172 | /** 173 | * Cuts a value from the session. The value will be returned and the key will be unset from the array. 174 | * 175 | * @param string $key The key to cut. Dot-notation can be used with nested arrays. 176 | * 177 | * @return mixed The value of the key, or null if the key does not exist. 178 | */ 179 | public static function cut(string $key) 180 | { 181 | return Globals::cutSession($key); 182 | } 183 | 184 | 185 | /** 186 | * Writes a flash message to the session. 187 | * This method can be invoked without arguments, in that case a `Flash` object will be returned. 188 | * 189 | * @param string $type [optional] Message type. 190 | * @param string $text [optional] Message text. 191 | * @param bool $now [optional] Whether to write and make the message available for rendering immediately or wait for the next request. 192 | * 193 | * @return Flash 194 | */ 195 | public static function flash(string $text = '', string $type = '', bool $now = false): Flash 196 | { 197 | static $flash = null; 198 | 199 | if ($flash === null) { 200 | $flash = new Flash(); 201 | } 202 | 203 | if (strlen(trim($text))) { 204 | $flash($type, $text, $now); 205 | } 206 | 207 | return $flash; 208 | } 209 | 210 | /** 211 | * Returns an instance of the CSRF class. 212 | * 213 | * @param string $name [optional] The name of the CSRF token. Default to `{session.csrf.name}` configuration value. 214 | * If a token name other than the default is specified, validation of this token has to be implemented manually. 215 | * 216 | * @return CSRF 217 | */ 218 | public static function csrf(?string $name = null): CSRF 219 | { 220 | return new CSRF($name); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /bootstrap/loader.php: -------------------------------------------------------------------------------- 1 | Path 22 | 'base' => BASE_PATH, 23 | ]; 24 | 25 | // autoloader directory to namespace mapping 26 | $namespaces = [ 27 | // DIR => Namespace Prefix 28 | 'classes' => 'MAKS\\Velox\\', 29 | 'app' => 'App\\', 30 | ]; 31 | 32 | // autoloader dynamically aliased aliased classes 33 | $aliases = [ 34 | // Alias => FQN 35 | 'App' => \MAKS\Velox\App::class, 36 | 'Auth' => \MAKS\Velox\Backend\Auth::class, 37 | 'Event' => \MAKS\Velox\Backend\Event::class, 38 | 'Config' => \MAKS\Velox\Backend\Config::class, 39 | 'Router' => \MAKS\Velox\Backend\Router::class, 40 | 'Globals' => \MAKS\Velox\Backend\Globals::class, 41 | 'Session' => \MAKS\Velox\Backend\Session::class, 42 | 'Database' => \MAKS\Velox\Backend\Database::class, 43 | 'Data' => \MAKS\Velox\Frontend\Data::class, 44 | 'View' => \MAKS\Velox\Frontend\View::class, 45 | 'HTML' => \MAKS\Velox\Frontend\HTML::class, 46 | 'Path' => \MAKS\Velox\Frontend\Path::class, 47 | ]; 48 | 49 | 50 | 51 | // include paths 52 | $paths = implode(PATH_SEPARATOR, [get_include_path(), ...array_values($paths)]); 53 | 54 | // autoloader function 55 | $loader = function ($class) use (&$loader, $namespaces, $aliases) { 56 | if (isset($aliases[$class])) { 57 | $loader($aliases[$class]); 58 | 59 | if (!class_exists($class)) { 60 | class_alias($aliases[$class], $class); 61 | } 62 | 63 | return; 64 | } 65 | 66 | foreach ($namespaces as $directory => $namespace) { 67 | if (strrpos($class, $namespace) !== false) { 68 | $ext = '.php'; 69 | $path = realpath(BASE_PATH . DIRECTORY_SEPARATOR . $directory); 70 | $name = str_replace([$namespace, '\\'], [DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR], $class); 71 | 72 | $file = $path . $name . $ext; 73 | 74 | if (file_exists($file)) { 75 | require_once($file); 76 | } 77 | } 78 | } 79 | }; 80 | 81 | // errors handler, makes everything an exception, even minor warnings 82 | $errorHandler = function (int $code, string $message, string $file, int $line) { 83 | if (!class_exists('ErrorOrWarningException')) { 84 | class ErrorOrWarningException extends \ErrorException {}; 85 | } 86 | 87 | throw new ErrorOrWarningException($message, $code, E_ERROR, $file, $line); 88 | }; 89 | 90 | // exceptions handler, logs the exception and then dumps it and/or displays a nice page 91 | $exceptionHandler = function (\Throwable $exception) { 92 | // explicitly set the status code in case it is not set 93 | http_response_code(500); 94 | 95 | // only keep the last buffer if nested 96 | while (ob_get_level() > 0) { 97 | ob_end_clean(); 98 | } 99 | 100 | // enable logging in case it is disabled 101 | \MAKS\Velox\Backend\Config::set('global.logging.enabled', true); 102 | 103 | // get app environment 104 | $environment = \MAKS\Velox\Backend\Config::get('global.env'); 105 | 106 | // log the exception 107 | \MAKS\Velox\App::log("[ERROR: (env: {$environment})] {$exception}", null, 'system'); 108 | 109 | if (in_array(strtoupper($environment), ['PROD', 'PRODUCTION'])) { 110 | // if in production environment, return a nice page without all the details 111 | $view = \MAKS\Velox\Backend\Config::get('global.errorPages.' . $exception->getCode()); 112 | 113 | if ($view) { 114 | // if exception code matches a error page code, then render it 115 | \MAKS\Velox\App::abort($exception->getCode(), null, $exception->getMessage()); 116 | } else { 117 | // otherwise, fall back to 500 error page 118 | \MAKS\Velox\App::abort(500, null, 'An error occurred, try again later.'); 119 | } 120 | 121 | } else { 122 | // if in development environment, dump a detailed exception 123 | \MAKS\Velox\Helper\Dumper::dumpException($exception); 124 | } 125 | 126 | // terminate the app entirely without executing shutdown functions 127 | \MAKS\Velox\App::terminate(null, false); 128 | }; 129 | 130 | // shutdown function, makes errors and exceptions handlers available at shutdown 131 | $shutdownFunction = function () use ($errorHandler, $exceptionHandler) { 132 | // die only if no shutdown is allowed 133 | if (\MAKS\Velox\Helper\Misc::getArrayValueByKey($GLOBALS['_VELOX'], 'TERMINATE', false)) { 134 | die; 135 | } 136 | 137 | // add App runtime shutdown methods 138 | \MAKS\Velox\App::extendStatic('handleError', $errorHandler); 139 | \MAKS\Velox\App::extendStatic('handleException', $exceptionHandler); 140 | 141 | // execute the shutdown event only if it is not already executed 142 | if (\MAKS\Velox\Helper\Misc::getArrayValueByKey($GLOBALS['_VELOX'], 'SHUTDOWN', true)) { 143 | \MAKS\Velox\App::shutdown(); 144 | } 145 | }; 146 | 147 | 148 | 149 | // set include paths 150 | set_include_path($paths); 151 | spl_autoload_register($loader, true, false); 152 | set_error_handler($errorHandler); 153 | set_exception_handler($exceptionHandler); 154 | register_shutdown_function($shutdownFunction); 155 | date_default_timezone_set(\MAKS\Velox\Backend\Config::get('global.timezone')); 156 | 157 | 158 | 159 | // clean up 160 | unset( 161 | $paths, 162 | $namespaces, 163 | $aliases, 164 | $loader, 165 | $exceptionHandler, 166 | $errorHandler, 167 | $shutdownFunction, 168 | ); 169 | 170 | 171 | 172 | /** 173 | * Requires a directory recursively. 174 | * 175 | * @param string $path The path to a directory. If the passed parameter is not a directory, this function will skip it. 176 | * 177 | * @return bool If something was included, it returns true, otherwise false. 178 | */ 179 | function require_recursive(string $directory): bool { 180 | static $files = []; 181 | 182 | if (!is_dir($directory)) { 183 | return false; 184 | } 185 | 186 | $filenames = scandir($directory) ?: []; 187 | foreach ($filenames as $filename) { 188 | $file = sprintf('%s/%s', $directory, $filename); 189 | 190 | if (is_dir($file)) { 191 | // only if subdirectory, not current or parent. 192 | if (strpos($filename, '.') === false) { 193 | require_recursive($file); 194 | } 195 | continue; 196 | } 197 | 198 | if (is_file($file)) { 199 | require_once($file); 200 | 201 | $files[] = $file; 202 | } 203 | } 204 | 205 | return (bool)count($files); 206 | } 207 | 208 | /** 209 | * Aliases classes in a directory to the root namespace recursively. Note that namespaces have to follow PSR-4. 210 | * 211 | * @param string $directory The path to a directory. If the passed parameter is not a directory, this function will skip it. 212 | * @param string $namespacePrefix The prefix for classes namespace. 213 | * 214 | * @return bool If something was aliased, it returns true, otherwise false. 215 | */ 216 | function class_alias_recursive(string $directory, string $namespacePrefix): bool { 217 | static $aliases = []; 218 | 219 | if (!is_dir($directory)) { 220 | return false; 221 | } 222 | 223 | $filenames = scandir($directory) ?: []; 224 | foreach ($filenames as $filename) { 225 | $file = sprintf('%s/%s', $directory, $filename); 226 | 227 | if (is_dir($file)) { 228 | // only if subdirectory, not current or parent. 229 | if (strpos($filename, '.') === false) { 230 | class_alias_recursive($file, $namespacePrefix); 231 | } 232 | continue; 233 | } 234 | 235 | if (is_file($file)) { 236 | $className = basename($file, '.php'); 237 | $classDirectory = str_replace(dirname($directory), '', dirname($file)); 238 | $classNamespace = sprintf('%s\\%s', trim($namespacePrefix, '\\'), trim($classDirectory, '/')); 239 | $classFQN = sprintf('%s\\%s', $classNamespace, $className); 240 | 241 | class_alias($classFQN, $className); 242 | $aliases[$className] = $classFQN; 243 | } 244 | } 245 | 246 | return (bool)count($aliases); 247 | } 248 | -------------------------------------------------------------------------------- /app/Controller/PersonsController.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright Marwan Al-Soltany 2021 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Controller; 13 | 14 | use MAKS\Velox\Backend\Controller; 15 | use App\Model\Person; 16 | 17 | class PersonsController extends Controller 18 | { 19 | /** 20 | * {@inheritDoc} 21 | */ 22 | protected function associateModel(): ?string 23 | { 24 | return Person::class; 25 | } 26 | 27 | /** 28 | * {@inheritDoc} 29 | */ 30 | protected function registerRoutes(): bool 31 | { 32 | return true; 33 | } 34 | 35 | /** 36 | * @return string 37 | */ 38 | public function indexAction(): string 39 | { 40 | $persons = $this->model->all(); 41 | 42 | return $this->view->render('persons/index', [ 43 | 'title' => 'Listing Persons', 44 | 'persons' => $persons 45 | ]); 46 | } 47 | 48 | /** 49 | * @return string 50 | */ 51 | public function createAction(): string 52 | { 53 | return $this->view->render('persons/create', [ 54 | 'title' => 'Create a new Person', 55 | ]); 56 | } 57 | 58 | /** 59 | * @return void 60 | */ 61 | public function storeAction(): void 62 | { 63 | $attributes = $this->globals->post->getAll(); 64 | 65 | // TODO: add some validation before setting the attributes 66 | $person = $this->model->create([ 67 | 'name' => $attributes['name'] ?? '', 68 | 'age' => $attributes['age'] ?? '', 69 | 'username' => $attributes['username'] ?? '', 70 | 'email' => $attributes['email'] ?? '', 71 | 'address' => $attributes['address'] ?? '', 72 | 'company' => $attributes['company'] ?? '', 73 | ]); 74 | 75 | $person->save(); 76 | 77 | $this->session->flash('A new person was created successfully!', 'notification'); 78 | 79 | $this->router->redirect('/persons'); 80 | } 81 | 82 | /** 83 | * @param string $path 84 | * @param string $id 85 | * 86 | * @return string 87 | */ 88 | public function showAction(string $path, ?string $id): string 89 | { 90 | $person = $this->model->find($id); 91 | $others = $this->model->where('id', '<>', $id); 92 | 93 | return $this->view->render('persons/show', [ 94 | 'title' => 'Showing: ' . $person->getName(), 95 | 'person' => $person, 96 | 'others' => $others 97 | ]); 98 | } 99 | 100 | /** 101 | * @param string $path 102 | * @param string $id 103 | * 104 | * @return string 105 | */ 106 | public function editAction(string $path, ?string $id): string 107 | { 108 | $person = $this->model->find($id); 109 | 110 | return $this->view->render('persons/edit', [ 111 | 'title' => 'Editing: ' . $person->getName(), 112 | 'person' => $person 113 | ]); 114 | } 115 | 116 | /** 117 | * @param string $path 118 | * @param string $id 119 | * 120 | * @return void 121 | */ 122 | public function updateAction(string $path, ?string $id): void 123 | { 124 | $person = $this->model->find($id); 125 | 126 | $attributes = $this->globals->post->getAll(); 127 | 128 | // TODO: add some validation before setting the attributes 129 | $person->setAttributes($attributes); 130 | 131 | $person->save(); 132 | 133 | $this->session->flash('Person with ID ' . $id . ' was updated successfully!', 'notification'); 134 | 135 | $this->router->redirect('/persons'); 136 | } 137 | 138 | /** 139 | * @param string $path 140 | * @param string $id 141 | * 142 | * @return void 143 | */ 144 | public function destroyAction(string $path, ?string $id): void 145 | { 146 | $person = $this->model->find($id); 147 | 148 | $person->delete(); 149 | 150 | $this->session->flash('Person with ID ' . $id . ' was deleted successfully!', 'notification'); 151 | 152 | $this->router->redirect('/persons'); 153 | } 154 | 155 | /** 156 | * @route("/persons/list", {GET}) 157 | * 158 | * @return void 159 | */ 160 | public function listAction(): void 161 | { 162 | $this->router->forward('/persons'); 163 | } 164 | 165 | /** 166 | * This method is used to seed the database with some test data. 167 | * 168 | * @param bool $keepOldData 169 | * 170 | * @return void 171 | */ 172 | public static function createTestData(bool $keepOldData = true): void 173 | { 174 | if (!$keepOldData) { 175 | array_map(fn ($person) => $person->delete(), Person::all()); 176 | } 177 | 178 | $persons = [ 179 | [ 180 | 'name' => 'Todd Riley', 181 | 'age' => 32, 182 | 'username' => 'todd.riley', 183 | 'email' => 'todd.riley@domain.tld', 184 | 'address' => 'Andre Street 3, 69841 Santiago City, Country', 185 | 'company' => 'Wesley Benson and Sons', 186 | ], 187 | [ 188 | 'name' => 'Clarence Graham', 189 | 'age' => 49, 190 | 'username' => 'clarence.graham', 191 | 'email' => 'clarence.graham@domain.tld', 192 | 'address' => 'Troy Street 62, 69572 Holloway City, Country', 193 | 'company' => 'Lena Sanchez LLC', 194 | ], 195 | [ 196 | 'name' => 'Nannie Obrien', 197 | 'age' => 28, 198 | 'username' => 'nannie.obrien', 199 | 'email' => 'nannie.obrien@domain.tld', 200 | 'address' => 'Barbara Street 64, 32953 Crawford City, Country', 201 | 'company' => 'Aiden Sanchez Inc.', 202 | ], 203 | [ 204 | 'name' => 'Stanley Holt', 205 | 'age' => 61, 206 | 'username' => 'stanley.holt', 207 | 'email' => 'stanley.holt@domain.tld', 208 | 'address' => 'Nicholas Street 89, 99114 Owens City, Country', 209 | 'company' => 'Olive Brock LLC', 210 | ], 211 | [ 212 | 'name' => 'Jeanette Cunningham', 213 | 'age' => 23, 214 | 'username' => 'jeanette.cunningham', 215 | 'email' => 'jeanette.cunningham@domain.tld', 216 | 'address' => 'Josephine Street 52, 99445 Hall City, Country', 217 | 'company' => 'Leona Johnston Inc.', 218 | ], 219 | [ 220 | 'name' => 'Tyler Cruz', 221 | 'age' => 52, 222 | 'username' => 'tyler.cruz', 223 | 'email' => 'tyler.cruz@domain.tld', 224 | 'address' => 'Floyd Street 23, 26375 Mason City, Sint Country', 225 | 'company' => 'Abbie Coleman LLC', 226 | ], 227 | [ 228 | 'name' => 'Walter Stewart', 229 | 'age' => 27, 230 | 'username' => 'walter.stewart', 231 | 'email' => 'walter.stewart@domain.tld', 232 | 'address' => 'Della Street 100, 62204 Huff City, Country', 233 | 'company' => 'Antonio Potter Ltd.', 234 | ], 235 | [ 236 | 'name' => 'Eric Lee', 237 | 'age' => 41, 238 | 'username' => 'eric.lee', 239 | 'email' => 'eric.lee@domain.tld', 240 | 'address' => 'Paul Street 13, 72687 Spencer City, Country', 241 | 'company' => 'Sylvia Schneider Corp', 242 | ], 243 | [ 244 | 'name' => 'Ruth Harmon', 245 | 'age' => 29, 246 | 'username' => 'ruth.harmon', 247 | 'email' => 'ruth.harmon@domain.tld', 248 | 'address' => 'Beatrice Street 99, 38186 Perez City, Country', 249 | 'company' => 'Andrew Poole LLC', 250 | ], 251 | [ 252 | 'name' => 'Jim Craig', 253 | 'age' => 65, 254 | 'username' => 'jim.craig', 255 | 'email' => 'jim.craig@domain.tld', 256 | 'address' => 'Kate Street 76, 54801 Harris City, Country', 257 | 'company' => 'Owen Ferguson Ltd.', 258 | ], 259 | ]; 260 | 261 | foreach ($persons as $person) { 262 | $person = new Person($person); 263 | $person->save(); 264 | } 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /classes/Backend/Exception.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright Marwan Al-Soltany 2021 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace MAKS\Velox\Backend; 13 | 14 | use MAKS\Velox\Helper\Misc; 15 | 16 | /** 17 | * A class that serves as a base exception class with helpers to assist with errors/exceptions handling. 18 | * 19 | * Example: 20 | * ``` 21 | * // throw an exception 22 | * $signature = 'YException:XException'; // YException extends YException and will get created if it does not exist. 23 | * Exception::throw($signature, $message, $code, $previous); 24 | * 25 | * // handle the passed callback in a safe context where errors get converted to exceptions 26 | * Exception::handle($callback, $signature, $message); 27 | * 28 | * // trigger an E_USER_* error, warning, notice, or deprecated with backtrace info 29 | * Exception::trigger($message, $severity); 30 | * ``` 31 | * 32 | * @package Velox\Backend 33 | * @since 1.5.5 34 | * @api 35 | */ 36 | class Exception extends \Exception 37 | { 38 | /** 39 | * Class constructor. 40 | * {@inheritDoc} 41 | * 42 | * @param string $message The Exception message. 43 | */ 44 | public function __construct(string $message, int $code = 0, \Throwable $previous = null) 45 | { 46 | parent::__construct($message, $code, $previous); 47 | } 48 | 49 | /** 50 | * Returns a string representation of the exception object. 51 | * 52 | * @return string 53 | */ 54 | public function __toString() 55 | { 56 | return Misc::interpolate('{class}: {message} [Code: {code}] {eol}{trace}{eol}', [ 57 | 'class' => static::class, 58 | 'code' => $this->getCode(), 59 | 'message' => $this->getMessage(), 60 | 'trace' => $this->getTraceAsString(), 61 | 'eol' => PHP_EOL, 62 | ]); 63 | } 64 | 65 | 66 | /** 67 | * Creates an exception class dynamically and returns its class FQN. 68 | * 69 | * @param string $signature 70 | * 71 | * @return string 72 | */ 73 | final protected static function create(string $signature): string 74 | { 75 | if (class_exists($signature, false)) { 76 | return '\\' . trim($signature, '\\'); 77 | } 78 | 79 | $namespace = static::class; 80 | $parent = static::class; 81 | $class = $signature = trim($signature, '\\'); 82 | 83 | if (strpos($signature, ':') !== false) { 84 | [$class, $parent] = explode(':', $signature, 2); 85 | 86 | if (strpos($class, '\\') !== false) { 87 | $namespace = implode('\\', explode('\\', $class, -1)); 88 | $parts = explode('\\', $class); 89 | $class = $parts[count($parts) - 1]; 90 | } 91 | 92 | $parent = class_exists($parent) && is_subclass_of($parent, \Exception::class) 93 | ? trim($parent, '\\') 94 | : static::class; 95 | } 96 | 97 | $content = Misc::interpolate( 98 | ' isset($trace['class']) ? "{$trace['class']}::" : '', 150 | 'suffix' => isset($trace['function']) ? "{$trace['function']}() failed!" : '', 151 | 'file' => $trace['file'], 152 | 'line' => $trace['line'], 153 | ]); 154 | } 155 | 156 | if ($previous instanceof \Throwable) { 157 | $message = $message . Misc::interpolate(': [{class}] {message}', [ 158 | 'class' => (new \ReflectionClass($previous))->getShortName(), 159 | 'message' => $previous->getMessage(), 160 | ]); 161 | } 162 | 163 | throw new $exception((string)$message, (int)$code, $previous); 164 | } 165 | 166 | /** 167 | * Handles the passed callback in a safe context where PHP errors (and exceptions) result in exceptions that can be caught. 168 | * 169 | * @param callable $callback The callback to be executed. 170 | * @param string $signature [optional] Exception signature. 171 | * This can be a class FQN like `SomeException` or `Namespace\SomeException` or a class FQN with a parent FQN like `Namespace\SomeException:RuntimeException`. 172 | * Note that exception class will be created at runtime if it does not exist. 173 | * @param string $message [optional] The exception message if the callback raised an error or throw an exception. 174 | * 175 | * @return void 176 | * 177 | * @throws \Exception 178 | */ 179 | public static function handle(callable $callback, ?string $signature = null, ?string $message = null): void 180 | { 181 | static $handler = null; 182 | 183 | if ($handler === null) { 184 | $handler = function (int $code, string $message, string $file, int $line) { 185 | throw new \ErrorException($message, $code, E_ERROR, $file, $line); 186 | }; 187 | } 188 | 189 | set_error_handler($handler, E_ALL); 190 | 191 | try { 192 | $callback(); 193 | } catch (\Throwable $exception) { 194 | $message = $message ?? Misc::interpolate('{method}() failed in {file} on line {line}', [ 195 | 'method' => __METHOD__, 196 | 'file' => $exception->getFile(), 197 | 'line' => $exception->getLine(), 198 | ]); 199 | $message = $message . ': ' . $exception->getMessage(); 200 | $code = $exception->getCode(); 201 | 202 | static::throw($signature ?? static::class, $message, $code); 203 | } finally { 204 | restore_error_handler(); 205 | } 206 | } 207 | 208 | /** 209 | * Triggers a user-level error, warning, notice, or deprecation with backtrace info. 210 | * 211 | * @param string $message Error message. 212 | * @param int $severity Error severity (`E_USER_*` family error). Default and fallback is `E_USER_ERROR`. 213 | * - `E_USER_ERROR => 256`, 214 | * - `E_USER_WARNING => 512`, 215 | * - `E_USER_NOTICE => 1024`, 216 | * - `E_USER_DEPRECATED => 16384`. 217 | * 218 | * @return void 219 | */ 220 | public static function trigger(string $message, int $severity = E_USER_ERROR): void 221 | { 222 | $trace = Misc::backtrace(['file', 'line'], 1); 223 | 224 | $error = Misc::interpolate('{message} in {file} on line {line} ', [ 225 | 'file' => $trace['file'], 226 | 'line' => $trace['line'], 227 | 'message' => $message, 228 | ]); 229 | 230 | $severities = [E_USER_ERROR, E_USER_WARNING, E_USER_NOTICE, E_USER_DEPRECATED]; 231 | 232 | trigger_error($error, in_array($severity, $severities, true) ? $severity : E_USER_ERROR); 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /classes/Frontend/Path.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright Marwan Al-Soltany 2021 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace MAKS\Velox\Frontend; 13 | 14 | use MAKS\Velox\Backend\Config; 15 | use MAKS\Velox\Backend\Globals; 16 | 17 | /** 18 | * A class that serves as a path resolver for different paths/URLs of the app. 19 | * 20 | * Example: 21 | * ``` 22 | * // NOTE: all methods that have "resolve" as prefix 23 | * // can also be called without the resolve prefix 24 | * // Path::resolve*() -> Path:*() like Path::resolveUrl() -> Path::url() 25 | * 26 | * // get an absolute path from app root 27 | * $path = Path::resolve('some/path'); 28 | * 29 | * // get a public URL from app root 30 | * $url = Path::resolveUrl('some/route'); 31 | * 32 | * 33 | * // get a relative path from theme root 34 | * $path = Path::resolveFromTheme('some/file.ext'); 35 | * 36 | * // get a public URL from theme root 37 | * $url = Path::resolveUrlFromTheme('some/file.ext'); 38 | * 39 | * 40 | * // get a relative path from theme assets root 41 | * $path = Path::resolveFromAssets('some/file.ext'); 42 | * 43 | * // get a public URL from theme assets root 44 | * $url = Path::resolveUrlFromAssets('some/file.ext'); 45 | * ``` 46 | * 47 | * @package Velox\Frontend 48 | * @since 1.0.0 49 | * @api 50 | * 51 | * @method static string url(string $path = '/') 52 | * @method static string fromTheme(string $path = '/', string $prefix = '') 53 | * @method static string urlFromTheme(string $path = '/') 54 | * @method static string fromAssets(string $path = '/', string $prefix = '') 55 | * @method static string urlFromAssets(string $path = '/') 56 | */ 57 | final class Path 58 | { 59 | /** 60 | * Returns the current path, or compares it with the passed parameter. Note that the path does not contain the query string. 61 | * 62 | * @param string|null $compareTo [optional] Some path on the server. 63 | * 64 | * @return string|bool If null is passed, the current path as string. Otherwise the result of comparing the current path with the passed parameter as boolean. 65 | */ 66 | public static function current(?string $compareTo = null) 67 | { 68 | $path = strtok(Globals::getServer('REQUEST_URI'), '?'); 69 | 70 | if ($compareTo) { 71 | return $path === $compareTo; 72 | } 73 | 74 | return $path; 75 | } 76 | 77 | /** 78 | * Returns the current URL, or compares it with the passed parameter. 79 | * 80 | * @param string|null $compareTo [optional] Some URL on the server. 81 | * 82 | * @return string|bool If null is passed, the current URL as string. Otherwise the result of comparing the current URL with the passed parameter as boolean. 83 | */ 84 | public static function currentUrl(?string $compareTo = null) 85 | { 86 | $url = static::resolveUrl((string)static::current()); 87 | 88 | if ($compareTo) { 89 | return $url === $compareTo; 90 | } 91 | 92 | return $url; 93 | } 94 | 95 | /** 96 | * Resolves the passed path to the app root path and returns it. 97 | * 98 | * @param string [optional] $path The path from app root. 99 | * 100 | * @return string An absolute path on the server starting from app root. 101 | */ 102 | public static function resolve(string $path = '/'): string 103 | { 104 | static $root = null; 105 | 106 | if ($root === null) { 107 | $root = Config::get('global.paths.root'); 108 | } 109 | 110 | $absolutePath = sprintf( 111 | '%s/%s', 112 | rtrim($root, '/'), 113 | ltrim($path, '/') 114 | ); 115 | 116 | $canonicalPath = realpath($absolutePath); 117 | 118 | return $canonicalPath ? $canonicalPath : $absolutePath; 119 | } 120 | 121 | /** 122 | * Resolves the passed path to the base URL (starting from app root) and returns it. 123 | * 124 | * @param string [optional] $path The path from app root. 125 | * 126 | * @return string An absolute path on the server (public URL) starting from app root. 127 | */ 128 | public static function resolveUrl(string $path = '/'): string 129 | { 130 | static $url = null; 131 | 132 | if ($url === null) { 133 | $url = Config::get('global.baseUrl', vsprintf('%s://%s', [ 134 | Globals::getServer('HTTPS') === 'on' ? 'https' : 'http', 135 | Globals::getServer('HTTP_HOST'), 136 | ])); 137 | } 138 | 139 | return sprintf( 140 | '%s/%s', 141 | rtrim($url, '/'), 142 | ltrim($path, '/') 143 | ); 144 | } 145 | 146 | /** 147 | * Resolves the passed path to the theme root path and returns it. 148 | * 149 | * @param string [optional] $path The path from theme root. 150 | * @param string [optional] $prefix The prefix to prefix the returned path with (base URL for example). 151 | * 152 | * @return string A relative path starting from app root to the root of the active theme directory. 153 | */ 154 | public static function resolveFromTheme(string $path = '/', string $prefix = ''): string 155 | { 156 | static $theme = null; 157 | 158 | if ($theme === null) { 159 | $theme = str_replace( 160 | Config::get('global.paths.root'), 161 | '', 162 | Config::get('theme.paths.root') 163 | ); 164 | } 165 | 166 | return sprintf( 167 | '%s/%s/%s', 168 | rtrim($prefix, '/'), 169 | trim($theme, '/'), 170 | ltrim($path, '/') 171 | ); 172 | } 173 | 174 | /** 175 | * Resolves the passed path to the base URL (starting from active theme root) and returns it. 176 | * 177 | * @param string [optional] $path The path from theme root. 178 | * 179 | * @return string An absolute path on the server (public URL) starting from active theme root. 180 | */ 181 | public static function resolveUrlFromTheme(string $path = '/'): string 182 | { 183 | return static::resolveFromTheme($path, static::resolveUrl()); 184 | } 185 | 186 | /** 187 | * Resolves the passed path to the assets directory and returns it. 188 | * 189 | * @param string [optional] $path The path from theme root assets root. 190 | * @param string [optional] $prefix The prefix to prefix the returned path with (base URL for example). 191 | * 192 | * @return string A relative path starting from app root to the root of the assets directory of the active theme directory. 193 | */ 194 | public static function resolveFromAssets(string $path = '/', string $prefix = ''): string 195 | { 196 | static $assets = null; 197 | 198 | if (!$assets) { 199 | $assets = str_replace( 200 | Config::get('theme.paths.root'), 201 | '', 202 | Config::get('theme.paths.assets') 203 | ); 204 | 205 | $assets = static::resolveFromTheme($assets); 206 | } 207 | 208 | return sprintf( 209 | '%s/%s/%s', 210 | rtrim($prefix, '/'), 211 | trim($assets, '/'), 212 | ltrim($path, '/') 213 | ); 214 | } 215 | 216 | /** 217 | * Resolves the passed path to the base URL (starting from active theme assets root) and returns it. 218 | * 219 | * @param string [optional] $path The path from theme root assets root. 220 | * 221 | * @return string An absolute path on the server (public URL) starting from active theme root. 222 | */ 223 | public static function resolveUrlFromAssets(string $path = '/'): string 224 | { 225 | return static::resolveFromAssets($path, static::resolveUrl()); 226 | } 227 | 228 | /** 229 | * Returns a normalized path based on OS. 230 | * 231 | * @param string $directory 232 | * @param string $filename 233 | * @param string $extension 234 | * 235 | * @return string 236 | */ 237 | public static function normalize(string $directory, string $filename, string $extension = ''): string 238 | { 239 | $filename = substr($filename, -strlen($extension)) === $extension ? $filename : $filename . $extension; 240 | $directory = $directory . '/'; 241 | 242 | return preg_replace('/(\/|\\\)+/', DIRECTORY_SEPARATOR, $directory . $filename); 243 | } 244 | 245 | 246 | /** 247 | * Aliases `self::resolve*()` with a function of the same name without the "resolve" prefix. 248 | */ 249 | public static function __callStatic(string $method, array $arguments) 250 | { 251 | $class = static::class; 252 | $method = sprintf('resolve%s', ucfirst($method)); 253 | 254 | if (!method_exists($class, $method)) { 255 | throw new \Exception("Call to undefined method {$class}::{$method}()"); 256 | } 257 | 258 | return static::$method(...$arguments); 259 | } 260 | 261 | /** 262 | * Allows static methods handled by `self::__callStatic()` to be accessible via object operator `->`. 263 | */ 264 | public function __call(string $method, array $arguments) 265 | { 266 | return static::__callStatic($method, $arguments); 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /classes/Backend/Config.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright Marwan Al-Soltany 2021 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace MAKS\Velox\Backend; 13 | 14 | use MAKS\Velox\App; 15 | use MAKS\Velox\Backend\Event; 16 | use MAKS\Velox\Helper\Misc; 17 | 18 | /** 19 | * A class that loads everything from the "/config" directory and make it as an array that is accessible via dot-notation. 20 | * 21 | * Example: 22 | * ``` 23 | * // get the entire config 24 | * $entireConfig = Config::getAll(); 25 | * 26 | * // check for config value availability 27 | * $varNameExists = Config::has('filename.config.varName'); 28 | * 29 | * // get a specific config value or fall back to a default value 30 | * $varName = Config::get('filename.config.varName', 'fallbackValue'); 31 | * 32 | * // set a specific config value at runtime 33 | * Config::set('filename.config.varName', 'varValue'); 34 | * 35 | * // delete cached config 36 | * Config::clearCache(); 37 | * ``` 38 | * 39 | * @package Velox\Backend 40 | * @since 1.0.0 41 | * @api 42 | */ 43 | class Config 44 | { 45 | /** 46 | * This event will be dispatched when the config is loaded. 47 | * This event will be passed a reference to the config array. 48 | * 49 | * @var string 50 | */ 51 | public const ON_LOAD = 'config.on.load'; 52 | 53 | /** 54 | * This event will be dispatched when the config is cached. 55 | * This event will not be passed any arguments. 56 | * 57 | * @var string 58 | */ 59 | public const ON_CACHE = 'config.on.cache'; 60 | 61 | /** 62 | * This event will be dispatched when the config cache is cleared. 63 | * This event will not be passed any arguments. 64 | * 65 | * @var string 66 | */ 67 | public const ON_CLEAR_CACHE = 'config.on.clearCache'; 68 | 69 | 70 | /** 71 | * The default directory of the configuration files. 72 | * 73 | * @var string 74 | */ 75 | public const CONFIG_DIR = BASE_PATH . '/config'; 76 | 77 | /** 78 | * The path of the cached configuration file. 79 | * 80 | * @var string 81 | */ 82 | public const CONFIG_CACHE_FILE = BASE_PATH . '/storage/cache/config/config.json'; 83 | 84 | 85 | /** 86 | * The currently loaded configuration. 87 | */ 88 | protected static array $config; 89 | 90 | 91 | public function __construct() 92 | { 93 | $this->load(); 94 | } 95 | 96 | public function __toString() 97 | { 98 | return static::CONFIG_DIR; 99 | } 100 | 101 | 102 | /** 103 | * Includes all files in a directory. 104 | * 105 | * @param string $path 106 | * 107 | * @return array 108 | */ 109 | private static function include(string $path): array 110 | { 111 | $includes = []; 112 | $include = static function ($file) { 113 | if (is_file($file)) { 114 | $info = pathinfo($file); 115 | return $info['extension'] == 'php' ? include($file) : []; 116 | } 117 | 118 | return self::include($file); 119 | }; 120 | 121 | // load all config files 122 | $filenames = scandir($path) ?: []; 123 | foreach ($filenames as $filename) { 124 | $file = sprintf('%s/%s', $path, $filename); 125 | $config = basename($filename, '.php'); 126 | 127 | // do not include items that have dots in their names 128 | // as this will conflict with array access separator 129 | if (strpos($config, '.') !== false) { 130 | continue; 131 | } 132 | 133 | $includes[$config] = isset($includes[$config]) 134 | ? $include($file) + (array)$includes[$config] 135 | : $include($file); 136 | } 137 | 138 | return $includes; 139 | } 140 | 141 | /** 142 | * Parses the configuration to replace reference of some `{filename.config.varName}` with actual value from the passed configuration. 143 | * 144 | * @param array $config 145 | * 146 | * @return array 147 | */ 148 | private static function parse(array $config): array 149 | { 150 | // parses all config variables 151 | $tries = count($config); 152 | for ($i = 0; $i < $tries; $i++) { 153 | array_walk_recursive($config, function (&$value) use (&$config) { 154 | if (is_string($value)) { 155 | if (preg_match_all('/{([a-z0-9_\-\.]*)}/i', $value, $matches)) { 156 | $variables = []; 157 | array_walk($matches[1], function (&$variable) use (&$variables, &$config) { 158 | $variables[$variable] = Misc::getArrayValueByKey($config, $variable, null); 159 | }); 160 | 161 | $value = $value === $matches[0][0] 162 | ? $variables[$matches[1][0]] 163 | : Misc::interpolate($value, $variables); 164 | } 165 | } 166 | }); 167 | } 168 | 169 | return $config; 170 | } 171 | 172 | /** 173 | * Loads the configuration (directly or when available from cache) and sets class internal state. 174 | * 175 | * @return void 176 | */ 177 | protected static function load(): void 178 | { 179 | $configDir = static::CONFIG_DIR; 180 | $configCacheFile = static::CONFIG_CACHE_FILE; 181 | 182 | if (empty(static::$config)) { 183 | if (file_exists($configCacheFile)) { 184 | $configJson = file_get_contents($configCacheFile); 185 | static::$config = json_decode($configJson, true); 186 | 187 | return; 188 | } 189 | 190 | static::$config = self::parse(self::include($configDir)); 191 | 192 | Event::dispatch(self::ON_LOAD, [&static::$config]); 193 | } 194 | } 195 | 196 | /** 197 | * Caches the current configuration as JSON. Note that a new version will not be generated unless the cache is cleared. 198 | * 199 | * @return void 200 | */ 201 | public static function cache(): void 202 | { 203 | $configDir = static::CONFIG_DIR; 204 | $configCacheFile = static::CONFIG_CACHE_FILE; 205 | $configCacheDir = dirname($configCacheFile); 206 | 207 | if (file_exists($configCacheFile)) { 208 | return; 209 | } 210 | 211 | if (!file_exists($configCacheDir)) { 212 | mkdir($configCacheDir, 0744, true); 213 | } 214 | 215 | $config = self::parse(self::include($configDir)); 216 | $configJson = json_encode($config, JSON_PRETTY_PRINT); 217 | 218 | file_put_contents($configCacheFile, $configJson, LOCK_EX); 219 | 220 | Event::dispatch(self::ON_CACHE); 221 | 222 | App::log( 223 | 'Generated cache for system config, checksum (SHA-256: {checksum})', 224 | ['checksum' => hash('sha256', $configJson)], 225 | 'system' 226 | ); 227 | } 228 | 229 | /** 230 | * Deletes the cached configuration JSON and resets class internal state. 231 | * 232 | * @return void 233 | */ 234 | public static function clearCache(): void 235 | { 236 | static::$config = []; 237 | 238 | $configCacheFile = static::CONFIG_CACHE_FILE; 239 | 240 | if (file_exists($configCacheFile)) { 241 | unlink($configCacheFile); 242 | } 243 | 244 | Event::dispatch(self::ON_CLEAR_CACHE); 245 | 246 | App::log('Cleared config cache', null, 'system'); 247 | } 248 | 249 | /** 250 | * Checks whether a value of a key exists in the configuration via dot-notation. 251 | * 252 | * @param string $key The dotted key representation. 253 | * 254 | * @return bool 255 | */ 256 | public static function has(string $key): bool 257 | { 258 | static::load(); 259 | 260 | $value = Misc::getArrayValueByKey(static::$config, $key, null); 261 | 262 | return isset($value); 263 | } 264 | 265 | /** 266 | * Gets a value of a key from the configuration via dot-notation. 267 | * 268 | * @param string $key The dotted key representation. 269 | * @param mixed $fallback [optional] The default fallback value. 270 | * 271 | * @return mixed The requested value or null. 272 | */ 273 | public static function get(string $key, $fallback = null) 274 | { 275 | static::load(); 276 | 277 | return Misc::getArrayValueByKey(static::$config, $key, $fallback); 278 | } 279 | 280 | /** 281 | * Sets a value of a key in the configuration via dot-notation. 282 | * 283 | * @param string $key The dotted key representation. 284 | * @param mixed $value The value to set. 285 | * 286 | * @return void 287 | */ 288 | public static function set(string $key, $value): void 289 | { 290 | static::load(); 291 | 292 | Misc::setArrayValueByKey(static::$config, $key, $value); 293 | } 294 | 295 | /** 296 | * Returns the currently loaded configuration. 297 | * 298 | * @return array 299 | */ 300 | public static function getAll(): array 301 | { 302 | static::load(); 303 | 304 | return static::getReference(); 305 | } 306 | 307 | /** 308 | * Returns a referenced to the current configuration array. 309 | * 310 | * @return array 311 | * 312 | * @since 1.5.5 313 | */ 314 | public static function &getReference(): array 315 | { 316 | static::load(); 317 | 318 | return static::$config; 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /classes/Backend/Controller.php: -------------------------------------------------------------------------------- 1 | data->set('page.title', 'Some Page'); 34 | * $someVar = $this->config->get('filename.someVar'); 35 | * return $this->view->render('some-page', $this->vars); 36 | * } 37 | * }; 38 | * 39 | * // use the created action as a handler for a route 40 | * Router::handle('/some-route', [$controller, 'someAction'], ['GET', 'POST']); 41 | * ``` 42 | * 43 | * @package Velox\Backend 44 | * @since 1.0.0 45 | * @api 46 | * 47 | * @property Event $event Instance of the `Event` class. 48 | * @property Config $config Instance of the `Config` class. 49 | * @property Router $router Instance of the `Router` class. 50 | * @property Globals $globals Instance of the `Globals` class. 51 | * @property Session $session Instance of the `Session` class. 52 | * @property Database $database Instance of the `Database` class. 53 | * @property Auth $auth Instance of the `Auth` class. 54 | * @property Data $data Instance of the `Data` class. 55 | * @property View $view Instance of the `View` class. 56 | * @property HTML $html Instance of the `HTML` class. 57 | * @property Path $path Instance of the `Path` class. 58 | * @property Dumper $dumper Instance of the `Dumper` class. 59 | * @property Misc $misc Instance of the `Misc` class. 60 | */ 61 | abstract class Controller 62 | { 63 | /** 64 | * This event will be dispatched when a controller (or a subclass) is constructed. 65 | * This event will not be passed any arguments, but its listener callback will be bound to the object (the controller class). 66 | * 67 | * @var string 68 | */ 69 | public const ON_CONSTRUCT = 'controller.on.construct'; 70 | 71 | 72 | /** 73 | * Preconfigured CRUD routes. 74 | * 75 | * @since 1.3.0 76 | */ 77 | private array $crudRoutes = [ 78 | 'index' => [ 79 | 'expression' => '/{controller}', 80 | 'method' => 'GET', 81 | ], 82 | 'create' => [ 83 | 'expression' => '/{controller}/create', 84 | 'method' => 'GET', 85 | ], 86 | 'store' => [ 87 | 'expression' => '/{controller}', 88 | 'method' => 'POST', 89 | ], 90 | 'show' => [ 91 | 'expression' => '/{controller}/([1-9][0-9]*)', 92 | 'method' => 'GET', 93 | ], 94 | 'edit' => [ 95 | 'expression' => '/{controller}/([1-9][0-9]*)/edit', 96 | 'method' => 'GET', 97 | ], 98 | 'update' => [ 99 | 'expression' => '/{controller}/([1-9][0-9]*)', 100 | 'method' => ['PUT', 'PATCH'], 101 | ], 102 | 'destroy' => [ 103 | 'expression' => '/{controller}/([1-9][0-9]*)', 104 | 'method' => 'DELETE', 105 | ], 106 | ]; 107 | 108 | /** 109 | * The passed variables array to the Controller. 110 | */ 111 | protected array $vars; 112 | 113 | protected ?Model $model; 114 | 115 | 116 | /** 117 | * Class constructor. 118 | * 119 | * @param array $vars [optional] Additional variables to pass to the controller. 120 | */ 121 | public function __construct(array $vars = []) 122 | { 123 | $this->vars = $vars; 124 | $this->model = null; 125 | 126 | if ($this->associateModel()) { 127 | $this->doAssociateModel(); 128 | } 129 | 130 | if ($this->registerRoutes()) { 131 | $this->doRegisterRoutes(); 132 | } 133 | 134 | Event::dispatch(self::ON_CONSTRUCT, null, $this); 135 | } 136 | 137 | public function __get(string $property) 138 | { 139 | if (isset(App::instance()->{$property})) { 140 | return App::instance()->{$property}; 141 | } 142 | 143 | Exception::throw( 144 | 'UndefinedPropertyException:OutOfBoundsException', 145 | sprintf('Call to undefined property %s::$%s', static::class, $property), 146 | ); 147 | } 148 | 149 | public function __isset(string $property) 150 | { 151 | return isset(App::instance()->{$property}); 152 | } 153 | 154 | 155 | /** 156 | * Controls which model should be used by current controller. 157 | * 158 | * This method should return a concrete class FQN of a model that extends `Model::class`. 159 | * 160 | * This method returns `null` by default. 161 | * 162 | * NOTE: If the model class does not exist, the controller will ignore it silently. 163 | * 164 | * @return string 165 | * 166 | * @since 1.3.0 167 | */ 168 | protected function associateModel(): ?string 169 | { 170 | return null; // @codeCoverageIgnore 171 | } 172 | 173 | /** 174 | * Whether or not to automatically register controller routes. 175 | * 176 | * NOTE: The controller class has to be instantiated at least once for this to work. 177 | * 178 | * Only public methods suffixed with the word `Action` or `Middleware` will be registered. 179 | * The suffix will determine the route type (`*Action` => `handler`, `*Middleware` => `middleware`). 180 | * The route will look like `/controller-name/method-name` (names will be converted to slugs). 181 | * The method will be `GET` by default. See also `self::$crudRoutes`. 182 | * You can use the `@route` annotation to overrides the default Route and Method. 183 | * The `@route` annotation can be used in DocBlock on a class method with the following syntax: 184 | * - Pattern: `@route("", {, ...})` 185 | * - Example: `@route("/some-route", {GET, POST})` 186 | * 187 | * This method returns `false` by default. 188 | * 189 | * @return bool 190 | * 191 | * @since 1.3.0 192 | */ 193 | protected function registerRoutes(): bool 194 | { 195 | return false; // @codeCoverageIgnore 196 | } 197 | 198 | /** 199 | * Associates a model class to the controller. 200 | * 201 | * @return void 202 | * 203 | * @since 1.3.0 204 | */ 205 | private function doAssociateModel(): void 206 | { 207 | $model = $this->associateModel(); 208 | // to prevent \ReflectionClass from throwing an exception 209 | $model = class_exists($model) ? $model : Model::class; 210 | 211 | $reflection = new \ReflectionClass($model); 212 | 213 | if ($reflection->isSubclassOf(Model::class) && !$reflection->isAbstract()) { 214 | $this->model = $reflection->newInstance(); 215 | } 216 | } 217 | 218 | /** 219 | * Registers all public methods which are suffixed with `Action` or `Middleware` as `handler` or `middleware` respectively. 220 | * 221 | * @return void 222 | * 223 | * @since 1.3.0 224 | */ 225 | private function doRegisterRoutes(): void 226 | { 227 | $class = new \ReflectionClass($this); 228 | $methods = $class->getMethods(\ReflectionMethod::IS_PUBLIC); 229 | 230 | foreach ($methods as $method) { 231 | $className = $class->getShortName(); 232 | $methodName = $method->getName(); 233 | $docBlock = $method->getDocComment() ?: ''; 234 | 235 | if ( 236 | $method->isAbstract() || 237 | $method->isStatic() || 238 | preg_match('/(Action|Middleware)$/', $methodName) === 0 239 | ) { 240 | continue; 241 | } 242 | 243 | $controller = Misc::transform(str_replace('Controller', '', $className), 'kebab', 'slug'); 244 | $handler = Misc::transform(str_replace(['Action', 'Middleware'], '', $methodName), 'kebab', 'slug'); 245 | 246 | $routes = $this->crudRoutes; 247 | 248 | if (!in_array($handler, array_keys($routes))) { 249 | Misc::setArrayValueByKey( 250 | $routes, 251 | $handler . '.expression', 252 | sprintf('/%s/%s', $controller, $handler) 253 | ); 254 | } 255 | 256 | if (preg_match('/(@route[ ]*\(["\'](.+)["\']([ ,]*\{(.+)\})?\))/', $docBlock, $matches)) { 257 | $routeExpression = $matches[2] ?? ''; 258 | $routeMethod = $matches[4] ?? ''; 259 | 260 | $routeMethod = array_filter(array_map('trim', explode(',', $routeMethod))); 261 | $routes[$handler] = [ 262 | 'expression' => $routeExpression, 263 | 'method' => $routeMethod, 264 | ]; 265 | } 266 | 267 | $function = preg_match('/(Middleware)$/', $methodName) ? 'middleware' : 'handle'; 268 | $expression = Misc::interpolate($routes[$handler]['expression'], ['controller' => $controller]); 269 | $method = $routes[$handler]['method'] ?? 'GET'; 270 | 271 | $this->router->{$function}($expression, [$this, $methodName], $method); 272 | } 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /classes/Backend/Database.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright Marwan Al-Soltany 2021 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace MAKS\Velox\Backend; 13 | 14 | use MAKS\Velox\Backend\Exception; 15 | 16 | /** 17 | * A class that represents the database and handles database operations. 18 | * 19 | * Example: 20 | * ``` 21 | * $database = Database::instance(); 22 | * $database->query('SELECT * FROM `users`'); 23 | * $database->prepare('SELECT * FROM `users` WHERE `job` = :job LIMIT 5')->execute([':job' => 'Developer'])->fetchAll(); 24 | * $database->perform('SELECT * FROM `users` WHERE `title` LIKE :title AND `id` > :id', ['title' => 'Dr.%', 'id' => 1])->fetchAll(); 25 | * ``` 26 | * 27 | * @package Velox\Backend 28 | * @since 1.3.0 29 | * @api 30 | */ 31 | class Database extends \PDO 32 | { 33 | /** 34 | * Current open database connections. 35 | */ 36 | protected static array $connections; 37 | 38 | /** 39 | * A cache to hold prepared statements. 40 | */ 41 | protected array $cache; 42 | 43 | protected string $dsn; 44 | protected ?string $username; 45 | protected ?string $password; 46 | protected ?array $options; 47 | 48 | 49 | /** 50 | * Class constructor. 51 | * 52 | * Adds some default options to the PDO connection. 53 | * 54 | * @param string $dsn 55 | * @param string|null $username 56 | * @param string|null $password 57 | * @param array|null $options 58 | */ 59 | protected function __construct(string $dsn, ?string $username = null, ?string $password = null, ?array $options = null) 60 | { 61 | $this->dsn = $dsn; 62 | $this->username = $username; 63 | $this->password = $password; 64 | $this->options = $options; 65 | 66 | $this->cache = []; 67 | 68 | parent::__construct($dsn, $username, $password, $options); 69 | 70 | $this->setAttribute(static::ATTR_ERRMODE, static::ERRMODE_EXCEPTION); 71 | $this->setAttribute(static::ATTR_DEFAULT_FETCH_MODE, static::FETCH_ASSOC); 72 | $this->setAttribute(static::ATTR_EMULATE_PREPARES, false); 73 | $this->setAttribute(static::MYSQL_ATTR_FOUND_ROWS, true); 74 | $this->setAttribute(static::ATTR_STATEMENT_CLASS, [$this->getStatementClass()]); 75 | } 76 | 77 | 78 | /** 79 | * Returns a singleton instance of the `Database` class based on connection credentials. 80 | * This method makes sure that a single connection is opened and reused for each connection credentials set (DSN, User, Password, ...). 81 | * 82 | * @param string|null $dsn The DSN string. 83 | * @param string|null $username [optional] The database username. 84 | * @param string|null $password [optional] The database password. 85 | * @param array|null $options [optional] PDO options. 86 | * 87 | * @return static 88 | */ 89 | final public static function connect(string $dsn, ?string $username = null, ?string $password = null, ?array $options = null): Database 90 | { 91 | $connection = md5(serialize(func_get_args())); 92 | 93 | if (!isset(static::$connections[$connection])) { 94 | static::$connections[$connection] = new static($dsn, $username, $password, $options); 95 | } 96 | 97 | return static::$connections[$connection]; 98 | } 99 | 100 | /** 101 | * Returns the singleton instance of the `Database` class using credentials found in `{database}` config. 102 | * 103 | * @return static 104 | * 105 | * @codeCoverageIgnore This method is overridden (mocked) in tests. 106 | */ 107 | public static function instance(): Database 108 | { 109 | $databaseConfig = Config::get('database', []); 110 | 111 | try { 112 | return static::connect( 113 | $databaseConfig['dsn'] ?? '', 114 | $databaseConfig['username'] ?? null, 115 | $databaseConfig['password'] ?? null, 116 | $databaseConfig['options'] ?? null 117 | ); 118 | } catch (\PDOException $error) { 119 | // connection can't be established (incorrect config), return a fake instance 120 | return static::mock(); 121 | } 122 | } 123 | 124 | /** 125 | * Returns FQN for a custom `PDOStatement` class. 126 | * 127 | * @return string 128 | */ 129 | private function getStatementClass(): string 130 | { 131 | $statement = new class () extends \PDOStatement { 132 | // Makes method chaining a little bit more convenient. 133 | #[\ReturnTypeWillChange] 134 | public function execute($params = null) 135 | { 136 | parent::execute($params); 137 | 138 | return $this; 139 | } 140 | // Catches the debug dump instead of printing it out directly. 141 | #[\ReturnTypeWillChange] 142 | public function debugDumpParams() 143 | { 144 | ob_start(); 145 | 146 | parent::debugDumpParams(); 147 | 148 | $dump = ob_get_contents(); 149 | ob_end_clean(); 150 | 151 | return $dump; 152 | } 153 | }; 154 | 155 | return get_class($statement); 156 | } 157 | 158 | /** 159 | * Adds caching capabilities for prepared statement. 160 | * {@inheritDoc} 161 | */ 162 | #[\ReturnTypeWillChange] 163 | public function prepare($query, $options = []) 164 | { 165 | $hash = md5($query); 166 | 167 | if (!isset($this->cache[$hash])) { 168 | $this->cache[$hash] = parent::prepare($query, $options); 169 | } 170 | 171 | return $this->cache[$hash]; 172 | } 173 | 174 | /** 175 | * A wrapper method to perform a query on the fly using either `self::query()` or `self::prepare()` + `self::execute()`. 176 | * 177 | * @param string $query The query to execute. 178 | * @param array $params The parameters to bind to the query. 179 | * 180 | * @return \PDOStatement 181 | */ 182 | public function perform(string $query, ?array $params = null): \PDOStatement 183 | { 184 | try { 185 | if (empty($params)) { 186 | return $this->query($query); 187 | } 188 | 189 | $statement = $this->prepare($query); 190 | $statement->execute($params); 191 | 192 | return $statement; 193 | } catch (\PDOException $error) { 194 | Exception::throw( 195 | 'QueryFailedException:PDOException', 196 | "Could not execute the query '{$query}'", 197 | (int)$error->getCode(), 198 | $error 199 | ); 200 | } 201 | } 202 | 203 | /** 204 | * Serves as a wrapper method to execute some operations in transactional context with the ability to attempt retires. 205 | * 206 | * @param callable $callback The callback to execute inside the transaction. This callback will be bound to the `Database` class. 207 | * @param int $retries The number of times to attempt the transaction. Each retry will be delayed by 1-3 seconds. 208 | * 209 | * @return mixed The result of the callback. 210 | * 211 | * @throws \RuntimeException If the transaction fails after all retries. 212 | */ 213 | public function transactional(callable $callback, int $retries = 3) 214 | { 215 | $callback = \Closure::fromCallable($callback)->bindTo($this); 216 | $attempts = 0; 217 | $return = null; 218 | 219 | do { 220 | $this->beginTransaction(); 221 | 222 | try { 223 | $return = $callback($this); 224 | 225 | $this->commit(); 226 | 227 | break; 228 | } catch (\Throwable $error) { 229 | $this->rollBack(); 230 | 231 | if (++$attempts === $retries) { 232 | Exception::throw( 233 | 'TransactionFailedException:RuntimeException', 234 | "Could not complete the transaction after {$retries} attempt(s).", 235 | (int)$error->getCode(), 236 | $error 237 | ); 238 | } 239 | 240 | sleep(rand(1, 3)); 241 | } finally { 242 | if ($this->inTransaction()) { 243 | $this->rollBack(); 244 | } 245 | } 246 | } while ($attempts < $retries); 247 | 248 | return $return; 249 | } 250 | 251 | /** 252 | * Returns a fake instance of the `Database` class. 253 | * 254 | * @return Database This instance will throw an exception if a method is called. 255 | * 256 | * @codeCoverageIgnore 257 | */ 258 | private static function mock() 259 | { 260 | return new class () extends Database { 261 | // only methods that raise an error or throw an exception are overridden 262 | protected function __construct() 263 | { 264 | // constructor arguments are not used 265 | } 266 | #[\ReturnTypeWillChange] 267 | public function exec($statement) 268 | { 269 | static::fail(); 270 | } 271 | #[\ReturnTypeWillChange] 272 | public function prepare($query, $options = []) 273 | { 274 | static::fail(); 275 | } 276 | #[\ReturnTypeWillChange] 277 | public function query($query, $fetchMode = null, ...$fetchModeArgs) 278 | { 279 | static::fail(); 280 | } 281 | #[\ReturnTypeWillChange] 282 | public function beginTransaction() 283 | { 284 | static::fail(); 285 | } 286 | #[\ReturnTypeWillChange] 287 | public function commit() 288 | { 289 | static::fail(); 290 | } 291 | #[\ReturnTypeWillChange] 292 | public function rollBack() 293 | { 294 | static::fail(); 295 | } 296 | 297 | private static function fail(): void 298 | { 299 | Exception::throw( 300 | 'ConnectionFailedException:LogicException', 301 | 'The app is currently running using a fake database, all database related operations will fail. ' . 302 | 'Add valid database credentials using "config/database.php" to resolve this issue' 303 | ); 304 | } 305 | }; 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /classes/Backend/Auth.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright Marwan Al-Soltany 2021 6 | * For the full copyright and license information, please view 7 | * the LICENSE file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace MAKS\Velox\Backend; 13 | 14 | use MAKS\Velox\App; 15 | use MAKS\Velox\Backend\Exception; 16 | use MAKS\Velox\Backend\Event; 17 | use MAKS\Velox\Backend\Config; 18 | use MAKS\Velox\Backend\Model; 19 | use MAKS\Velox\Backend\Globals; 20 | use MAKS\Velox\Backend\Session; 21 | 22 | /** 23 | * A class that serves as an authentication system for users. 24 | * 25 | * Example: 26 | * ``` 27 | * // register a new user 28 | * $auth = new Auth(); // or Auth::instance(); 29 | * $status = $auth->register('username', 'password'); 30 | * 31 | * // unregister a user 32 | * $status = Auth::instance()->unregister('username'); 33 | * 34 | * // log in a user 35 | * $status = Auth::instance()->login('username', 'password'); 36 | * 37 | * // log out a user 38 | * Auth::instance()->logout(); 39 | * 40 | * // authenticate a user model 41 | * Auth::authenticate($user); 42 | * 43 | * // check if there is a logged in user 44 | * $status = Auth::check(); 45 | * 46 | * // retrieve the current authenticated user 47 | * $user = Auth::user(); 48 | * 49 | * // add HTTP basic auth 50 | * Auth::basic(['username' => 'password']); 51 | * ``` 52 | * 53 | * @package Velox\Backend 54 | * @since 1.4.0 55 | * @api 56 | */ 57 | class Auth 58 | { 59 | /** 60 | * This event will be dispatched when an auth user is registered. 61 | * This event will be passed the user model object and its listener callback will be bound to the object (the auth class). 62 | * This event is useful if the user model class has additional attributes other than the `username` and `password` that need to be set. 63 | * 64 | * @var string 65 | */ 66 | public const ON_REGISTER = 'auth.on.register'; 67 | 68 | /** 69 | * This event will be dispatched after an auth user is registered. 70 | * This event will be passed the user model object and its listener callback will be bound to the object (the auth class instance). 71 | * 72 | * @var string 73 | */ 74 | public const AFTER_REGISTER = 'auth.after.register'; 75 | 76 | /** 77 | * This event will be dispatched when an auth user is unregistered. 78 | * This event will be passed the user model object and its listener callback will be bound to the object (the auth class instance). 79 | * 80 | * @var string 81 | */ 82 | public const ON_UNREGISTER = 'auth.on.unregister'; 83 | 84 | /** 85 | * This event will be dispatched when an auth user is logged in. 86 | * This event will be passed the user model object and its listener callback will be bound to the object (the auth class instance). 87 | * 88 | * @var string 89 | */ 90 | public const ON_LOGIN = 'auth.on.login'; 91 | 92 | /** 93 | * This event will be dispatched when an auth user is logged out. 94 | * This event will be passed the user model object and its listener callback will be bound to the object (the auth class instance). 95 | * 96 | * @var string 97 | */ 98 | public const ON_LOGOUT = 'auth.on.logout'; 99 | 100 | 101 | /** 102 | * The class singleton instance. 103 | */ 104 | protected static self $instance; 105 | 106 | 107 | /** 108 | * Auth user model. 109 | */ 110 | protected Model $user; 111 | 112 | 113 | /** 114 | * Class constructor. 115 | * 116 | * @param string $model [optional] The auth user model class to use. 117 | */ 118 | public function __construct(?string $model = null) 119 | { 120 | if (empty(static::$instance)) { 121 | static::$instance = $this; 122 | } 123 | 124 | $this->user = $this->getUserModel($model); 125 | 126 | $this->check(); 127 | } 128 | 129 | 130 | /** 131 | * Returns the singleton instance of the class. 132 | * 133 | * NOTE: This method returns only the first instance of the class 134 | * which is normally the one that was created during application bootstrap. 135 | * 136 | * @return static 137 | */ 138 | final public static function instance(): self 139 | { 140 | if (empty(static::$instance)) { 141 | static::$instance = new static(); 142 | } 143 | 144 | return static::$instance; 145 | } 146 | 147 | /** 148 | * Registers a new user. 149 | * 150 | * @param string $username Auth user username. 151 | * @param string $password Auth user password. 152 | * 153 | * @return bool True if the user was registered successfully, false if the user is already registered. 154 | */ 155 | public function register(string $username, string $password): bool 156 | { 157 | $user = $this->user->one([ 158 | 'username' => $username, 159 | ]); 160 | 161 | if ($user instanceof Model) { 162 | return false; 163 | } 164 | 165 | $user = $this->user->create([ 166 | 'username' => $username, 167 | 'password' => $this->hash($password), 168 | ]); 169 | 170 | Event::dispatch(self::ON_REGISTER, [$user], $this); 171 | 172 | $user->save(); 173 | 174 | Event::dispatch(self::AFTER_REGISTER, [$user], $this); 175 | 176 | return true; 177 | } 178 | 179 | /** 180 | * Unregisters a user. 181 | * 182 | * @param string $username Auth user username. 183 | * 184 | * @return bool True if the user was unregistered successfully, false if the user is not registered. 185 | */ 186 | public function unregister(string $username): bool 187 | { 188 | $user = $this->user->one([ 189 | 'username' => $username, 190 | ]); 191 | 192 | if (!$user) { 193 | return false; 194 | } 195 | 196 | if ($this->check()) { 197 | $this->logout(); 198 | } 199 | 200 | Event::dispatch(self::ON_UNREGISTER, [$user], $this); 201 | 202 | $user->delete(); 203 | 204 | return true; 205 | } 206 | 207 | /** 208 | * Logs in a user. 209 | * 210 | * @param string $username Auth user username. 211 | * @param string $password Auth user password. 212 | * 213 | * @return bool True if the user was logged in successfully, false if the user is not registered or the password is incorrect. 214 | */ 215 | public function login(string $username, string $password): bool 216 | { 217 | $user = $this->user->one([ 218 | 'username' => $username, 219 | ]); 220 | 221 | if ( 222 | $user instanceof Model && 223 | ( 224 | $this->verify($password, $user->getPassword()) || 225 | $password === $user->getPassword() // self::authenticate() will pass a hashed password 226 | ) 227 | ) { 228 | Session::set('_auth.username', $username); 229 | Session::set('_auth.timeout', time() + Config::get('auth.user.timeout', 3600)); 230 | 231 | Event::dispatch(self::ON_LOGIN, [$user], $this); 232 | 233 | return true; 234 | } 235 | 236 | return false; 237 | } 238 | 239 | /** 240 | * Logs out a user. 241 | * 242 | * @return void 243 | */ 244 | public function logout(): void 245 | { 246 | $user = $this->user(); 247 | 248 | Session::cut('_auth'); 249 | 250 | Event::dispatch(self::ON_LOGOUT, [$user], $this); 251 | } 252 | 253 | /** 254 | * Authenticates an auth user model. 255 | * 256 | * @param Model $user The auth user model to authenticate. 257 | * 258 | * @return void 259 | * 260 | * @throws \DomainException If the user could not be authenticated or the model is not an auth user model. 261 | */ 262 | public static function authenticate(Model $user): void 263 | { 264 | $instance = static::instance(); 265 | 266 | $success = false; 267 | 268 | Exception::handle( 269 | function () use ($instance, $user, &$success) { 270 | $success = $instance->login( 271 | $user->getUsername(), 272 | $user->getPassword() 273 | ); 274 | }, 275 | 'AuthenticationFailedException:DomainException', 276 | "Could not authenticate the model, the model may not be a valid auth user model" 277 | ); 278 | 279 | if (!$success) { 280 | Exception::throw( 281 | 'AuthenticationFailedException:DomainException', 282 | "Could not authenticate auth user with ID '{$user->getId()}'", 283 | ); 284 | } 285 | } 286 | 287 | /** 288 | * Checks if a user is logged in and logs the user out if the timeout has expired. 289 | * 290 | * @return bool 291 | */ 292 | public static function check(): bool 293 | { 294 | if (Session::get('_auth.timeout') <= time()) { 295 | Session::cut('_auth'); 296 | } 297 | 298 | if (Session::has('_auth')) { 299 | return true; 300 | } 301 | 302 | return false; 303 | } 304 | 305 | /** 306 | * Returns the authenticated user model instance. 307 | * 308 | * @return Model|null The authenticated user or null if no user has logged in. 309 | */ 310 | public static function user(): ?Model 311 | { 312 | if ($username = Session::get('_auth.username')) { 313 | return static::getUserModel()->findByUsername($username)[0] ?? null; 314 | } 315 | 316 | return null; 317 | } 318 | 319 | /** 320 | * Serves as an HTTP Basic Authentication guard for the specified logins. 321 | * 322 | * @param array $logins The login data, an associative array where key is the `username` and value is the `password`. 323 | * 324 | * @return void 325 | * 326 | * @throws \InvalidArgumentException If no logins where provided. 327 | * 328 | * @codeCoverageIgnore Can't test methods that send headers. 329 | */ 330 | public static function basic(array $logins) 331 | { 332 | if (count($logins) === 0) { 333 | Exception::throw( 334 | 'BadLoginCredentialsException:InvalidArgumentException', 335 | 'No valid login(s) provided', 336 | ); 337 | } 338 | 339 | $username = Globals::getServer('PHP_AUTH_USER'); 340 | $password = Globals::getServer('PHP_AUTH_PW'); 341 | 342 | $isAuthenticated = false; 343 | foreach ($logins as $user => $pass) { 344 | if ($username === $user && $password === $pass) { 345 | $isAuthenticated = true; 346 | 347 | break; 348 | } 349 | } 350 | 351 | header('Cache-Control: no-cache, must-revalidate, max-age=0'); 352 | 353 | if (!$isAuthenticated) { 354 | header('HTTP/1.1 401 Authorization Required'); 355 | header('WWW-Authenticate: Basic realm="Access denied"'); 356 | 357 | self::fail(); 358 | } 359 | } 360 | 361 | /** 362 | * Renders 401 error page. 363 | * 364 | * @return void 365 | * 366 | * @codeCoverageIgnore Can't test methods that send headers. 367 | */ 368 | public static function fail(): void 369 | { 370 | App::log('Responded with 401 to the request for "{uri}". Authentication failed. Client IP address {ip}', [ 371 | 'uri' => Globals::getServer('REQUEST_URI'), 372 | 'ip' => Globals::getServer('REMOTE_ADDR'), 373 | ], 'system'); 374 | 375 | App::abort(401, null, 'You need to be logged in to view this page!'); 376 | } 377 | 378 | /** 379 | * Hashes a password. 380 | * 381 | * @param string $password 382 | * 383 | * @return string The hashed password. 384 | */ 385 | protected function hash(string $password): string 386 | { 387 | $hashingConfig = Config::get('auth.hashing'); 388 | 389 | return password_hash($password, $hashingConfig['algorithm'] ?? PASSWORD_DEFAULT, [ 390 | 'cost' => $hashingConfig['cost'] ?? 10, 391 | ]); 392 | } 393 | 394 | /** 395 | * Verifies a password. 396 | * 397 | * @param string $password 398 | * @param string $hash 399 | * 400 | * @return bool 401 | */ 402 | protected function verify(string $password, string $hash): bool 403 | { 404 | return password_verify($password, $hash); 405 | } 406 | 407 | /** 408 | * Returns an instance of the user model class specified in the config or falls back to the default one. 409 | * 410 | * @param string $model [optional] The auth user model class to use. 411 | * 412 | * @return Model 413 | */ 414 | protected static function getUserModel(?string $model = null): Model 415 | { 416 | $model = $model ?? Config::get('auth.user.model'); 417 | 418 | $model = class_exists((string)$model) 419 | ? new $model() 420 | : new class () extends Model { 421 | public static ?string $table = 'users'; 422 | public static ?string $primaryKey = 'id'; 423 | public static ?array $columns = ['id', 'username', 'password']; 424 | public static function schema(): string 425 | { 426 | return ' 427 | CREATE TABLE IF NOT EXISTS `users` ( 428 | `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, 429 | `username` VARCHAR(255) NOT NULL UNIQUE, 430 | `password` VARCHAR(255) NOT NULL 431 | ); 432 | '; 433 | } 434 | }; 435 | 436 | Config::set('auth.user.model', get_class($model)); 437 | 438 | return $model; 439 | } 440 | } 441 | --------------------------------------------------------------------------------