├── .editorconfig ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── composer.json ├── composer.lock ├── phpunit.xml ├── src ├── App.php ├── Config │ ├── app │ │ ├── engines.php │ │ └── settings.php │ └── database.php ├── Console │ ├── Commands │ │ ├── Controller │ │ │ └── make.php │ │ ├── Keys │ │ │ └── generate.php │ │ ├── Middleware │ │ │ └── make.php │ │ ├── Migrations │ │ │ ├── make.php │ │ │ ├── migrate.php │ │ │ ├── refresh.php │ │ │ ├── reset.php │ │ │ └── rollback.php │ │ ├── Models │ │ │ └── make.php │ │ ├── Seeders │ │ │ ├── make.php │ │ │ └── seed.php │ │ └── Server │ │ │ └── serve.php │ ├── Line.php │ └── Parts │ │ ├── Migrations │ │ ├── model.php │ │ └── rollback.php │ │ └── verifyAndCreateDirectory.php.php ├── Database │ └── Manager.php ├── Exceptions │ └── InvalidParameterTypeException.php ├── Http │ ├── Request.php │ ├── Response.php │ └── Router.php └── Orbis │ ├── Http │ └── Router.php │ └── helpers.php └── tests ├── AppTest.php ├── Database ├── .env ├── .gitignore └── ManagerTest.php └── Http ├── RequestTest.php └── RouterTest.php /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 4 9 | end_of_line = crlf 10 | charset = utf-8 11 | trim_trailing_whitespace = false 12 | insert_final_newline = false 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /vendor 5 | 6 | # PHPStorm 7 | .idea 8 | 9 | # Composer 10 | composer.phar 11 | 12 | # Ignore system files 13 | .DS_Store 14 | Thumbs.db 15 | 16 | # Ignore local configuration files 17 | .phpunit.result.cache 18 | 19 | #Lithe Storage 20 | /storage -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.4.0] - 2025-1-09 4 | 5 | ### Features 6 | - Automatic modular routing system implemented. 7 | - Support for hierarchical route mounting based on folder structure. 8 | - Middleware inheritance applied to child routes. 9 | - Enhanced scalability and code organization for routing. 10 | 11 | ### Improvements 12 | - Simplified route management with automatic detection and setup. 13 | - Clear separation of parent and child routers for better modularity. 14 | 15 | Enjoy the new streamlined routing experience! 16 | 17 | ## [1.3.4] - 2024-12-05 18 | 19 | ### Fixed 20 | - **Fixed router system compatibility issue on Linux/Unix systems**: 21 | - Resolved an issue where route handling was failing on Linux/Unix systems due to incorrect file formatting and path visibility issues. 22 | - Adjusted file and directory permissions to ensure compatibility with Unix-based systems. 23 | - Improved the handling of system-specific configurations, ensuring the router functions properly across all platforms. 24 | 25 | ## [1.3.2] - 2024-11-14 26 | 27 | ### Fixed 28 | - **Corrected router instance check in the `any` function**: 29 | - Updated the `any` function to properly verify if `$router` is an instance of `Router`. 30 | - Adjusted the condition to throw an exception only when `$router` is not a valid `Router` instance. 31 | - Improved error handling when the router instance is missing, increasing function reliability. 32 | 33 | ### Refactored 34 | - **Enhanced route registration and creation with Orbis in the `Lithe\App` class**: 35 | - Replaced `Orbis::instance` with `Orbis::unregister` when registering routes, ensuring the instance is discarded after use. 36 | - Improved the `createRouterFromFile` method in the `Lithe\App` class to verify that the object returned from Orbis is an instance of `Router`. 37 | - Added extra validations and error messages for missing or invalid router configuration files. 38 | - Enhanced error handling and logging for cases where route registration fails. 39 | 40 | ## [1.3.1] - 2024-11-09 41 | 42 | ### Fixed 43 | - **Adjusted mapping for 'index.php' files in subdirectories**: 44 | - Updated the logic to map 'index.php' in the root of the application to `/`: 45 | - 'index.php' at the root is now mapped to `/`. 46 | - 'subdir/index.php' is now mapped to `/subdir/index`, maintaining the subdirectory structure. 47 | 48 | ## [1.3.0] - 2024-11-08 49 | 50 | ### Modified 51 | - **Add support for defining route directories using the set method**: 52 | - Implemented the `set` method to dynamically configure route directories, providing more flexibility in defining the base directory for routes. 53 | - Refactored route loading logic to support configurable directories, simplifying customization of route sources. 54 | - Improved route file inclusion to avoid redundancy, ensuring cleaner and more efficient route handling. 55 | 56 | ## [1.2.3] - 2024-11-07 57 | 58 | ### Modified 59 | - **Adds template validation for make: migration, model, and seeders commands**: If the template configured in the environment variable does not exist, a default template is created automatically. 60 | 61 | - **Refactor of param and extractCookies methods**: 62 | - **param**: Updated to handle URL-decoded values directly. 63 | - **extractCookies**: Replaced the anonymous object with a class featuring dynamic properties, allowing direct access to cookie values through `__get`, `__set`, and an `exists` method for existence checks. 64 | These improvements enhance the handling of parameters and cookies in the system. 65 | 66 | ## [1.2.2] - 2024-11-03 67 | 68 | ### Modified 69 | - **Refactor query parameter handling**: Changed to a direct property access method using `__get` to simplify access to query parameters. 70 | 71 | ## [1.2.1] - 2024-10-31 72 | 73 | ### Modified 74 | - **make:seeder command update**: Modified the `make:seeder` command to default to the environment connection method (`DB_CONNECTION_METHOD`) when no template option is specified. 75 | - **Template validation enhancement**: Improved validation to ensure that the specified template is valid before proceeding with file creation, providing a clearer error message if invalid. 76 | - **Enhanced feedback messages**: Updated the feedback messages for better user experience, confirming success or reporting errors when creating seeder files. 77 | 78 | ## [1.2.0] - 2024-10-28 79 | 80 | ### Added 81 | - **Create .env file for test configuration**: Added a `.env` file to facilitate test configuration. 82 | - **Add support for Seeder commands**: Implemented the structure to support seeder commands. 83 | - **Add make:seeder and db:seed commands**: New commands introduced for creating and executing seeders. 84 | 85 | ### Modified 86 | - **Remove .env from .gitignore**: The `.env` file was removed from `.gitignore`, allowing its inclusion in the repository. 87 | - **Update Model generation templates**: Updates to model generation templates for better compatibility and organization. 88 | - **Refactor middleware template to use class-based definition**: Refactored middleware templates to utilize class-based definitions, improving code readability and structure. 89 | 90 | ## [1.1.5] - Input Method Enhancements and Query Parameter Handling 91 | 92 | - **Description**: This version introduces enhancements to the input handling in the Lithe , allowing for more flexible retrieval of request data from both the body and query parameters. 93 | - **Changes**: 94 | - **Enhance input method to include query parameters**: 95 | - Updated the `input` method to retrieve values from both the request body and query parameters, improving usability. 96 | - **Improve migration file naming convention**: 97 | - Updated migration file naming format to include the day for better organization: `YYYY_MM_DD_HHMMSS_name.php`. 98 | - **Refactor getHost method to use protocol method**: 99 | - Modified the `getHost` function to utilize the protocol method for determining the scheme, enhancing code readability. 100 | 101 | ## [1.1.4] - 2024-10-25 102 | 103 | ### Changes 104 | - **Updated the getHost() and secure() functions**: 105 | - Enhanced the `secure()` function to properly check for secure requests, including support for proxies. 106 | - Modified the `getHost()` function to construct the host URL based on the secure request check. 107 | 108 | ## [1.1.3] - 2024-10-25 109 | 110 | ### Fixes 111 | - **cookie function**: 112 | - Fixed the validation for the `expire` option to ensure it is an integer. 113 | - Enhanced handling of the `expire` parameter to convert string values to Unix timestamps when needed. 114 | - Resolved an issue where setting cookies would throw a "option 'expire' is invalid" error. 115 | 116 | ### Improvements 117 | - Improved error handling in the `cookie` function for better robustness. 118 | 119 | 120 | ## [1.1.2] - Bug Fixes and Parameter Adjustments 121 | 122 | - **Description**: This version addresses minor bugs and makes adjustments to method parameter orders, improving the overall stability of the Lithe framework. 123 | - **Changes**: 124 | - **Fix case sensitivity issue for config path in App.php**: 125 | - Resolved issues related to case sensitivity for configuration paths in the App.php file. 126 | - **Fixing the render method call in Response**: 127 | - Adjusted the parameter order in the render method to resolve compatibility issues. 128 | - **Adjusting the order of parameters and method calls in engines.php**: 129 | - Rearranged parameters in function calls to avoid deprecation warnings. 130 | 131 | ## [1.1.0] - Improvements to Routing System and Performance 132 | 133 | - **Description**: This version focuses on enhancing the routing system and the overall performance of the Lithe framework, making it even lighter and more efficient. 134 | - **Changes**: 135 | - **Routing System Enhancements**: 136 | - Implemented improvements in route management to optimize performance and reduce complexity. 137 | - Removed event handling methods from the `App` class, simplifying the code structure. 138 | - **Database Connection Access**: 139 | - Removed direct access to the `DB_CONNECTION` constant. Now, the connection can be accessed through the new `connection()` method of the `Manager` class. 140 | - **Force Database Initialization**: 141 | - Modifications to ensure that the database initializes even if the environment variable `DB_SHOULD_INITIATE` is set to `false`. 142 | 143 | ## [1.0.2] - Refactor Router File Handling to be Case-Insensitive 144 | 145 | - **Description**: This version refactors the router's file handling to be case-insensitive, improving consistency and reliability when dealing with file paths. 146 | - **Changes**: 147 | - Modified the method for obtaining the file path in the router to ensure case-insensitive handling. 148 | - Updated the router registration in Orbis to ensure key comparison is case-insensitive. 149 | 150 | ## [1.0.1] - Updated Blade Cache Directory Path 151 | 152 | - **Description**: This version updates the Blade view rendering function to change the cache directory path initialization from using `PROJECT_ROOT` to `dirname(__DIR__, 6)`. This adjustment enhances the flexibility and maintainability of the cache directory structure while ensuring compatibility with the project’s directory layout. 153 | 154 | ## [1.0.0] - Initial Release 155 | 156 | - **Description**: Lithe is a PHP framework inspired by Express.js, renowned for its lightweight and flexible nature. It offers a minimalist approach to web development, integrating various components, ORMs, and databases while maintaining agile and efficient performance. 157 | 158 | - **Key Features**: 159 | - **Routing**: Simple and expressive route management with methods like `get()`, `post()`, among others. 160 | - **Middleware**: Robust support for middleware to handle requests, responses, and add functionalities such as authentication and logging. 161 | - **Templates**: Support for multiple template engines, including PHP Pure, Blade, and Twig, with easy configuration. 162 | - **Database Integration**: Integrated support for various ORMs and database drivers, including Eloquent, MySQLi, and PDO. Simple configuration through `.env` file and support for automated migrations. 163 | - **Migration Flexibility**: Ability to perform migrations with any database approach, including custom SQL queries or ORM-based migrations. 164 | - **Package Manager**: Lithe includes an integrated package manager to simplify the addition and management of modules and packages within your application. 165 | 166 | - **Achievements**: 167 | - **Agile Development**: Implementation of a lightweight framework that promotes rapid and intuitive development. 168 | - **Flexibility**: Seamless integration of various components and libraries, offering high flexibility for developers. 169 | - **Documentation**: Provision of clear and comprehensive documentation to facilitate effective use of the framework. 170 | - **Testing**: Support for testing with PHPUnit and Mockery to ensure code quality and reliability. 171 | - **Ease of Use**: User-friendly interfaces and abstractions designed to simplify the creation and maintenance of web applications. 172 | - **Database Integration**: Ease of configuration and management of connections to different databases, making integration with data management systems more efficient and flexible. 173 | - **Migration Capabilities**: Support for a variety of migration approaches, allowing developers to manage schema changes flexibly with either ORM tools or custom SQL scripts. -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Lithe 2 | 3 | Thank you for your interest in contributing to **Lithe**! We're excited to work with the community to improve Lithe even further. Please follow the guidelines below to ensure your contributions are effective and valuable to the project. 4 | 5 | ## How to Contribute 6 | 7 | ### Reporting Issues 8 | 9 | If you encounter a bug or want to suggest a new feature, please open an issue in the [Issues](https://github.com/lithephp/framework/issues) section of GitHub. When reporting an issue, include as much detail as possible: 10 | 11 | - A descriptive title. 12 | - Steps to reproduce the issue (if applicable). 13 | - Expected behavior vs actual behavior. 14 | - Screenshots or code examples (if applicable). 15 | - Version of Lithe and PHP being used. 16 | 17 | ### Fork the Repository 18 | 19 | Start by forking the repository to your GitHub account, and then clone it to your local machine: 20 | 21 | ```bash 22 | git clone https://github.com/lithephp/framework.git 23 | ``` 24 | 25 | ### Create a Branch 26 | 27 | Create a new branch to work on your feature or bug fix. Name your branch descriptively based on the change you're making: 28 | 29 | ```bash 30 | git checkout -b feature/my-new-feature 31 | ``` 32 | 33 | ### Make Changes 34 | 35 | Make the necessary changes to the code. We encourage you to: 36 | 37 | - Follow Lithe's coding standards and best practices. 38 | - Ensure the code is clean, efficient, and well-documented. 39 | - Include comments and documentation where necessary. 40 | 41 | ### Test Your Changes 42 | 43 | Before submitting a pull request, make sure of the following: 44 | 45 | - Run the relevant tests (if applicable). 46 | - Ensure your code hasn’t broken any existing functionality. 47 | - Add tests for any new functionality, if applicable. 48 | 49 | ### Commit Your Changes 50 | 51 | After making changes, commit them with a clear and concise commit message. Follow the standard commit message format: 52 | 53 | ```bash 54 | git commit -m "Descriptive message about the change" 55 | ``` 56 | 57 | ### Push Your Changes 58 | 59 | Push your branch to your forked repository: 60 | 61 | ```bash 62 | git push origin feature/my-new-feature 63 | ``` 64 | 65 | ### Submit a Pull Request 66 | 67 | After pushing your changes, go to the original repository and submit a pull request: 68 | 69 | - Include a detailed description of your changes. 70 | - Reference any related issues (e.g., `Fixes #123`). 71 | - Add any relevant documentation or examples if necessary. 72 | 73 | ## Code of Conduct 74 | 75 | We expect all contributors to follow the [Contributor Covenant Code of Conduct](https://www.contributor-covenant.org/version/2/0/code_of_conduct/). This ensures a welcoming and respectful environment for everyone. 76 | 77 | ## Style Guide 78 | 79 | To maintain consistency in the project, please adhere to the following guidelines: 80 | 81 | - **PHP Coding Standards**: Follow the PSR-12 standards for PHP code. 82 | - **Naming Conventions**: Use camelCase for methods and variables, and PascalCase for class names. 83 | - **File Structure**: Ensure new files are placed in the appropriate directories. Respect the project’s file organization. 84 | 85 | ### Tests 86 | 87 | - **PHPUnit** is used for testing. Be sure to write tests for any new features or bug fixes. 88 | - Ensure all tests pass before submitting a pull request. 89 | 90 | ### Documentation 91 | 92 | - Ensure your code is well-documented, especially for new features or changes to existing features. 93 | - If your contribution adds or modifies functionality, update the relevant sections of the documentation. 94 | 95 | ## License 96 | 97 | By contributing to this repository, you agree that your contributions will be licensed under the project's license (MIT). 98 | 99 | --- 100 | 101 | Thank you for contributing to **Lithe**! -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2024] Lithe 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lithe 2 | 3 |

4 | Lithe Logo 5 |

6 | 7 |

8 | Total Downloads 9 | Latest Stable Version 10 | License 11 |

12 | 13 | ## What is Lithe? 14 | 15 | Lithe is a PHP framework known for its simplicity, flexibility, and efficiency. Inspired by Express.js, Lithe is designed to help developers build web applications quickly and effectively. The name "Lithe" reflects the core characteristics of the framework: flexible and agile. 16 | 17 | ## Simple and Flexible Routing 18 | 19 | In Lithe, defining routes is very simple. You can use methods like `get()`, `post()`, and others to create routes that respond to different types of HTTP requests: 20 | 21 | ```php 22 | get('/hello/:name', function ($req, $res) { 23 | $res->send('Hello, ' . $req->param('name')); 24 | }); 25 | ``` 26 | 27 | Discover how [routing in Lithe](https://lithephp.vercel.app/docs/the-basics/routing) can simplify your development and offer complete control over your application's routes. 28 | 29 | ## Powerful Middleware 30 | 31 | In Lithe, middleware is your line of defense, allowing you to inspect, filter, and manipulate HTTP requests before they reach the final routes. Imagine adding functionalities like authentication and logging in a modular and reusable way! 32 | 33 | Here’s how easy it is to define and use middleware: 34 | 35 | ```php 36 | // Middleware to check if the token is valid 37 | $EnsureTokenIsValid = function ($req, $res, $next) { 38 | $token = $req->param('token'); 39 | 40 | if ($token !== 'my-secret-token') { 41 | $res->send('Invalid token.'); 42 | } 43 | 44 | $next(); 45 | }; 46 | 47 | // Protected route using the middleware 48 | get('/protected/:token', $EnsureTokenIsValid, function ($req, $res) { 49 | $res->send('Protected content accessed successfully!'); 50 | }); 51 | ``` 52 | 53 | Learn more about [middlewares in Lithe](https://lithephp.vercel.app/docs/the-basics/middleware) and see how they can transform the way you develop and maintain your applications. 54 | 55 | ## Database Integration 56 | 57 | Connecting to databases is straightforward with Lithe. The framework supports popular ORMs like Eloquent and native PHP drivers such as MySQLi and PDO. Configure your connections in the `.env` file and manage schema migrations easily. 58 | 59 | ``` 60 | DB_CONNECTION_METHOD=eloquent 61 | DB_CONNECTION=mysql 62 | DB_HOST=localhost 63 | DB_NAME=lithe 64 | DB_USERNAME=root 65 | DB_PASSWORD= 66 | DB_SHOULD_INITIATE=true 67 | ``` 68 | 69 | Learn more about [database integration in Lithe](https://lithephp.vercel.app/docs/database/integration) and see how easy it is to manage your data. 70 | 71 | ## Database Migrations 72 | 73 | Maintain consistency and integrity of data in your applications with automated migrations. With Lithe, you can create and apply migrations quickly and easily using any ORM interface or database driver. 74 | 75 | ```bash 76 | php line make:migration CreateUsersTable --template=eloquent 77 | php line migrate 78 | ``` 79 | 80 | Learn more about [migrations in Lithe](https://lithephp.vercel.app/docs/database/migrations) and make the most of this feature to build robust and scalable applications. 81 | 82 | ## Contributing 83 | 84 | Contributions are welcome! If you find an issue or have a suggestion, feel free to open an [issue](https://github.com/lithephp/framework/issues) or submit a [pull request](https://github.com/lithephp/framework/pulls). 85 | 86 | ## License 87 | 88 | Lithe is licensed under the [MIT License](https://opensource.org/licenses/MIT). See the [LICENSE](LICENSE) file for more details. 89 | 90 | ## Contact 91 | 92 | If you have any questions or need support, get in touch: 93 | 94 | - **Instagram**: [@lithephp](https://instagram.com/lithephp) 95 | - **Discord**: [Lithe](https://discord.gg/nfskM6x9x7) 96 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lithephp/framework", 3 | "description": "Lithe is a flexible and efficient PHP framework for creating robust web applications that adapt to developers' needs.", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "William Humbwavali", 9 | "homepage": "https://instagram.com/redeedite" 10 | } 11 | ], 12 | "autoload": { 13 | "psr-4": { 14 | "Lithe\\": "src/" 15 | }, 16 | "files": [ 17 | "src/Orbis/Http/Router.php", 18 | "src/Orbis/helpers.php" 19 | ] 20 | }, 21 | "require": { 22 | "php": "^8.2", 23 | "symfony/console": "^5.0", 24 | "lithemod/log": "^1.0", 25 | "lithemod/flow": "^1.0", 26 | "lithemod/import": "^1.0", 27 | "lithemod/env": "^1.0", 28 | "lithemod/validator": "^1.0", 29 | "lithemod/upload": "^1.0", 30 | "lithemod/orbis": "^1.0", 31 | "lithemod/httpexception": "^1.0" 32 | }, 33 | "require-dev": { 34 | "phpunit/phpunit": "11.2", 35 | "mockery/mockery": "^1.6" 36 | }, 37 | "scripts": { 38 | "test": "vendor/bin/phpunit" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | tests 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/App.php: -------------------------------------------------------------------------------- 1 | Settings = $settings; 39 | return $settings; 40 | } 41 | 42 | /** 43 | * Sets a template engine for the application. 44 | * 45 | * @param string $name The name of the template engine. 46 | * @param callable $config A configuration function for the template engine. 47 | * 48 | * @return void 49 | */ 50 | public function engine(string $name, callable $config): void 51 | { 52 | // Calls the static Settings method to configure the template engine. 53 | // This method manages general application settings. 54 | self::getSettings('view engine', $name, $config); 55 | } 56 | 57 | 58 | // Default application settings 59 | private $Options = [ 60 | "view engine" => "default", // Sets the default view engine 61 | 'views' => PROJECT_ROOT . '/views', // Specifies the default directory for views 62 | 'routes' => '', 63 | ]; 64 | 65 | /** 66 | * Sets a value for a configuration in the application. 67 | * 68 | * @param string $name The name of the configuration. 69 | * @param mixed $value The value to be assigned to the configuration. 70 | * @throws InvalidArgumentException if the option does not exist. 71 | */ 72 | public function set(string $name, mixed $value) 73 | { 74 | // Checks if the provided option is in the allowed list 75 | if (!in_array($name, self::ALLOWED_OPTIONS)) { 76 | // Throws an exception if the option does not exist in the allowed list 77 | throw new \InvalidArgumentException("The option '$name' does not exist."); 78 | } 79 | 80 | // Sets the configuration with the provided value 81 | $this->Options[$name] = $value; 82 | } 83 | 84 | 85 | /** 86 | * Creates and returns an instance of the Request object. 87 | * 88 | * This method initializes a new Request object with the provided parameters and URL. 89 | * The Request object encapsulates details such as URL parameters, HTTP headers, 90 | * and request body contents, providing a unified interface to interact with the 91 | * HTTP request data. 92 | * 93 | * @param object $parameters An object of parameters to be included in the request. 94 | * @param string $url The URL associated with the request. 95 | * @return \Lithe\Http\Request An instance of the Request object. 96 | */ 97 | private function Request(object $parameters, string $url): \Lithe\Http\Request 98 | { 99 | // Imports the Request.php file and returns its result, which is expected to be an instance of \Lithe\Http\Request. 100 | return import::with(compact('parameters', 'url')) 101 | ->file(__DIR__ . '/Http/Request.php'); 102 | } 103 | 104 | 105 | /** 106 | * Response object for handling HTTP responses. 107 | * 108 | * Retrieves an instance of the Response object with settings and options. 109 | * 110 | * @return \Lithe\Http\Response 111 | */ 112 | private function Response(): \Lithe\Http\Response 113 | { 114 | // Retrieves settings and options 115 | $Settings = $this->Settings; 116 | $Options = $this->Options; 117 | 118 | // Imports the Response.php file with settings and options and returns its result, 119 | // which should be an instance of \Lithe\Http\Response. 120 | return import::with(compact('Settings', 'Options')) 121 | ->file(__DIR__ . '/Http/Response.php'); 122 | } 123 | 124 | /** 125 | * Defines the handler for specific HTTP exceptions. 126 | * 127 | * @param int $status The HTTP status code for which the handler will be defined. 128 | * @param callable $handler The handler to deal with the HTTP exception. It should accept Lithe\Http\Request and \Lithe\Http\Response as parameters. 129 | * @return void 130 | */ 131 | public function fail(int $status, callable $handler): void 132 | { 133 | $this->HttpExceptions[$status] = $handler; 134 | } 135 | 136 | /** 137 | * Handles a specific HTTP error if a handler is registered for it. 138 | * 139 | * @param int $statusCode HTTP status code. 140 | * @param \Lithe\Http\Request $request The HTTP request object. 141 | * @param \Lithe\Http\Response $response The HTTP response object. 142 | * @param mixed $exception Optional. The exception that caused the HTTP error. 143 | * @return void 144 | */ 145 | private function handleHttpException(int $statusCode, \Lithe\Http\Request $request, \Lithe\Http\Response $response, $exception = null): void 146 | { 147 | // Call error handler for the specific status code if defined 148 | $errorHandler = $this->HttpExceptions[$statusCode] ?? null; 149 | if ($errorHandler && is_callable($errorHandler)) { 150 | // Determine arguments to pass to the error handler callback 151 | $args = [$request, $response]; 152 | if ($exception instanceof \Exception) { 153 | $args[] = $exception; 154 | } 155 | // Call the error handler callback with appropriate arguments 156 | call_user_func_array($errorHandler, $args); 157 | } 158 | } 159 | 160 | /** 161 | * Método que inicia o carregamento das rotas e monta a hierarquia. 162 | */ 163 | public function loadRoutes() 164 | { 165 | $routesDir = $this->Options['routes']; 166 | if (!is_dir($routesDir)) { 167 | return; 168 | } 169 | 170 | // 1. Varre recursivamente o diretório e obtém todos os arquivos PHP 171 | $files = $this->scanDirectory($routesDir); 172 | 173 | // 2. Para cada arquivo, calcula a rota correspondente e obtém o objeto Router 174 | // A chave será a rota (ex.: '/', '/cart', '/cart/dest', etc) 175 | $routers = []; 176 | foreach ($files as $file) { 177 | if (pathinfo($file, PATHINFO_EXTENSION) !== 'php') { 178 | continue; 179 | } 180 | $routeName = $this->computeRouteName($file, $routesDir); 181 | $router = $this->getRouterFromFile($file); 182 | if ($router) { 183 | $routers[$routeName] = $router; 184 | } 185 | } 186 | 187 | // 3. Ordena os roteadores pela profundidade (rotas mais profundas primeiro) 188 | uksort($routers, function ($a, $b) { 189 | return strlen($b) - strlen($a); 190 | }); 191 | 192 | // 4. Percorre os roteadores e, se houver um pai para a rota atual, monta-o como sub-router 193 | foreach ($routers as $routeName => $router) { 194 | if ($routeName === '/' || $routeName === '') { 195 | continue; // Rota raiz não tem pai 196 | } 197 | $parentRoute = $this->getParentRoute($routeName); 198 | if ($parentRoute !== false && isset($routers[$parentRoute])) { 199 | // Calcula o caminho relativo que será usado ao montar no pai. 200 | $relative = substr($routeName, strlen($parentRoute)); 201 | if ($relative === false || $relative === '') { 202 | $relative = '/'; 203 | } 204 | if ($relative[0] !== '/') { 205 | $relative = '/' . $relative; 206 | } 207 | // Monta o roteador filho no pai 208 | $routers[$parentRoute]->use($relative, $router); 209 | // Remove da lista de roteadores de nível superior, pois já está montado 210 | unset($routers[$routeName]); 211 | } 212 | } 213 | 214 | // 5. Por fim, monta os roteadores restantes (normalmente a raiz e outros de nível superior) na aplicação 215 | foreach ($routers as $routeName => $router) { 216 | $this->use($routeName, $router); 217 | } 218 | } 219 | 220 | /** 221 | * Varre recursivamente um diretório em busca de arquivos PHP. 222 | * 223 | * @param string $directory Diretório a ser escaneado. 224 | * @return array Lista dos caminhos completos dos arquivos PHP. 225 | */ 226 | private function scanDirectory(string $directory): array 227 | { 228 | $files = []; 229 | $items = array_diff(scandir($directory), ['.', '..']); 230 | foreach ($items as $item) { 231 | $path = "$directory/$item"; 232 | if (is_dir($path)) { 233 | $files = array_merge($files, $this->scanDirectory($path)); 234 | } elseif (is_file($path) && pathinfo($path, PATHINFO_EXTENSION) === 'php') { 235 | $files[] = $path; 236 | } 237 | } 238 | return $files; 239 | } 240 | 241 | /** 242 | * Calcula o nome da rota com base no caminho do arquivo. 243 | * Remove o diretório base e a extensão, normalizando as barras. 244 | * 245 | * @param string $file Caminho completo do arquivo. 246 | * @param string $baseDir Diretório base das rotas. 247 | * @return string Nome da rota (ex.: '/', '/cart', '/cart/dest'). 248 | */ 249 | private function computeRouteName(string $file, string $baseDir): string 250 | { 251 | $route = str_replace([$baseDir, '.php'], '', $file); 252 | $route = str_replace(DIRECTORY_SEPARATOR, '/', $route); 253 | $route = rtrim($route, '/'); 254 | return $route === '' ? '/' : $route; 255 | } 256 | 257 | /** 258 | * Obtém o nome da rota pai com base na rota atual. 259 | * 260 | * Exemplo: para "/cart/dest", retorna "/cart". 261 | * 262 | * @param string $route Rota atual. 263 | * @return mixed Rota pai ou false se não houver. 264 | */ 265 | private function getParentRoute(string $route) 266 | { 267 | if ($route === '/' || $route === '') { 268 | return false; 269 | } 270 | $parts = explode('/', ltrim($route, '/')); 271 | array_pop($parts); 272 | $parent = '/' . implode('/', $parts); 273 | return $parent === '' ? '/' : $parent; 274 | } 275 | 276 | /** 277 | * Obtém o objeto Router a partir do arquivo. 278 | * 279 | * Tenta incluir o arquivo, que deve retornar uma instância de \Lithe\Http\Router, 280 | * ou tenta recuperar uma instância já registrada. 281 | * 282 | * @param string $file Caminho do arquivo. 283 | * @return \Lithe\Http\Router|null Retorna o objeto Router ou null em caso de erro. 284 | */ 285 | private function getRouterFromFile(string $file): ?\Lithe\Http\Router 286 | { 287 | try { 288 | $key = strtolower(str_replace('/', DIRECTORY_SEPARATOR, $file)); 289 | Orbis::register(\Lithe\Http\Router::class, $key); 290 | $router = require($file); 291 | if ($router instanceof \Lithe\Http\Router) { 292 | return $router; 293 | } else { 294 | return $this->createRouterFromFile($key); 295 | } 296 | } catch (\Exception $e) { 297 | error_log("Erro ao registrar rota do arquivo {$file}: " . $e->getMessage()); 298 | Log::error("Erro ao registrar rota do arquivo {$file}: " . $e->getMessage()); 299 | return null; 300 | } 301 | } 302 | 303 | /** 304 | * Cria ou recupera o Router a partir do container usando a chave informada. 305 | * 306 | * @param string $key Chave única baseada no caminho do arquivo. 307 | * @return \Lithe\Http\Router 308 | * @throws \Exception Caso não seja encontrado o Router. 309 | */ 310 | private function createRouterFromFile(string $key): \Lithe\Http\Router 311 | { 312 | $router = Orbis::instance($key, true); 313 | if (!$router instanceof \Lithe\Http\Router) { 314 | throw new \Exception("Router não encontrado para a chave {$key}"); 315 | } 316 | return $router; 317 | } 318 | 319 | /** 320 | * Listens for incoming requests, processes them, and handles errors. 321 | * 322 | * @return void 323 | */ 324 | public function listen() 325 | { 326 | // Initialize settings if not already set 327 | $this->Settings = $this->Settings ?? $this->getSettings(); 328 | $this->loadRoutes(); 329 | // Match the request to a route 330 | $matchedRouteInfo = $this->findRouteAndParams(); 331 | 332 | ['route' => $route, 'params' => $params] = $matchedRouteInfo; 333 | 334 | // Get the request and response objects 335 | $request = $this->Request($params, $this->url()); 336 | $response = $this->Response(); 337 | 338 | Orbis::register($response, '\Lithe\Http\Response'); 339 | 340 | try { 341 | 342 | // Run global middlewares 343 | $this->runMiddlewares($this->middlewares, $request, $response, function () use ($request, $response, $route) { 344 | if ($route) { 345 | // Handle the matched route 346 | $this->handleRoute($route, $request, $response); 347 | } else { 348 | // Route not found, throw a 404 Not Found exception 349 | throw new \Lithe\Exceptions\Http\HttpException(404, 'Not found'); 350 | } 351 | }); 352 | } catch (\Lithe\Exceptions\Http\HttpException $e) { 353 | // Handle HTTP exceptions (e.g., 404 Not Found) 354 | $this->handleHttpException($e->getStatusCode(), $request, $response, $e); 355 | $this->sendErrorPage($response, $e->getStatusCode(), $e->getMessage()); 356 | } catch (\Exception $e) { 357 | // Handle general exceptions 358 | \Lithe\Support\Log::error($e->getMessage()); 359 | $this->handleHttpException(500, $request, $response, $e); 360 | $this->sendErrorPage($response, 500, 'Internal Server Error'); 361 | } 362 | } 363 | 364 | /** 365 | * Processes the current URL to remove the project context and returns the clean path. 366 | * 367 | * This function handles URLs both at the root (localhost:8000) and in subdirectories (localhost/project). 368 | * 369 | * @return string The clean URL path, starting with a slash. 370 | */ 371 | private function url(): string 372 | { 373 | // Get the full URL of the request. 374 | $requestUri = $_SERVER['REQUEST_URI']; 375 | 376 | // Get the path of the currently executing script. 377 | $scriptName = $_SERVER['SCRIPT_NAME']; 378 | 379 | // Extract the project context by removing the script name from the script path. 380 | // For example, if SCRIPT_NAME is "/project/index.php", projectContext will be "/project/". 381 | $projectContext = str_replace(basename($scriptName), '', $scriptName); 382 | 383 | // Parse the full URL to get just the path. 384 | // For example, if REQUEST_URI is "/project/home", path will be "/project/home". 385 | $path = parse_url($requestUri, PHP_URL_PATH); 386 | 387 | // If the path starts with the project context, remove that context from the path. 388 | // This adjusts the path to work correctly even in subdirectories. 389 | if (strpos($path, $projectContext) === 0) { 390 | // Substring removes the project context from the start of the path. 391 | $path = substr($path, strlen($projectContext)); 392 | } 393 | 394 | // Return the clean path, ensuring it starts with a slash. 395 | // This adds a slash at the beginning and removes extra slashes from the start. 396 | return '/' . ltrim($path, '/'); 397 | } 398 | 399 | /** 400 | * Finds the matching route for the current request and extracts parameters from the URL. 401 | * 402 | * @return array An array containing the matched route and the extracted parameters, or an empty array if no route matches. 403 | */ 404 | private function findRouteAndParams(): array 405 | { 406 | // Get the HTTP method and URL from the request 407 | $method = $_SERVER['REQUEST_METHOD']; 408 | $url = $this->url(); 409 | 410 | // Iterate through routes to find a match 411 | foreach ($this->routes as $route) { 412 | if ($route['method'] === $method && $this->matchRoute($route['route'], $url)) { 413 | // Extract parameters from the URL 414 | $params = $this->extractParams($route['route'], $url); 415 | 416 | // Return the matched route and extracted parameters 417 | return [ 418 | 'route' => $route, 419 | 'params' => $params 420 | ]; 421 | } 422 | } 423 | 424 | // No route matched 425 | return [ 426 | 'route' => null, 427 | 'params' => new class() { 428 | public function __get($name) 429 | { 430 | return $this->$name ?? null; 431 | } 432 | } 433 | ]; 434 | } 435 | 436 | /** 437 | * Handles the processing of a matched route. 438 | * 439 | * @param array $route The route information. 440 | * @param \Lithe\Http\Request $request The HTTP request. 441 | * @param \Lithe\Http\Response $response The HTTP response. 442 | * @return void 443 | */ 444 | private function handleRoute(array $route, \Lithe\Http\Request $request, \Lithe\Http\Response $response) 445 | { 446 | // Run route-specific middlewares 447 | $this->runMiddlewares($route['handler'], $request, $response, function () use ($response) { 448 | // End the response 449 | $response->end(); 450 | }); 451 | } 452 | 453 | /** 454 | * Executa a cadeia de middlewares. 455 | * 456 | * @param array $middlewares Lista de middlewares. 457 | * @param mixed $request Objeto ou dados da requisição. 458 | * @param mixed $response Objeto ou dados da resposta. 459 | * @param callable $next Callback a ser chamado ao final da cadeia. 460 | */ 461 | protected function runMiddlewares($middlewares, $request, $response, $next) 462 | { 463 | if (empty($middlewares)) { 464 | $next(); 465 | return; 466 | } 467 | 468 | $index = 0; 469 | 470 | // Função interna para chamar o próximo middleware 471 | $runNextMiddleware = function () use ($middlewares, $request, $response, &$index, $next, &$runNextMiddleware) { 472 | if ($index < count($middlewares)) { 473 | $middleware = $middlewares[$index++]; 474 | 475 | // Se o middleware for um array no formato [controller, 'method'] 476 | if (is_array($middleware) && count($middleware) === 2) { 477 | [$controller, $method] = $middleware; 478 | 479 | if (is_string($controller)) { 480 | $refMethod = new \ReflectionMethod($controller, $method); 481 | if ($refMethod->isStatic()) { 482 | // Middleware estático 483 | $middleware = function ($req, $res, $next) use ($controller, $method) { 484 | $controller::$method($req, $res, $next); 485 | }; 486 | } else { 487 | // Middleware não estático: instancia o controlador 488 | $instance = new $controller(); 489 | $middleware = function ($req, $res, $next) use ($instance, $method) { 490 | $instance->$method($req, $res, $next); 491 | }; 492 | } 493 | } elseif (is_object($controller)) { 494 | // Se já for uma instância, chama o método diretamente 495 | $middleware = function ($req, $res, $next) use ($controller, $method) { 496 | $controller->$method($req, $res, $next); 497 | }; 498 | } 499 | } 500 | 501 | // Executa o middleware atual 502 | $middleware($request, $response, $runNextMiddleware); 503 | } else { 504 | // Quando todos os middlewares forem executados, chama o callback final 505 | $next(); 506 | } 507 | }; 508 | 509 | // Inicia a execução dos middlewares 510 | $runNextMiddleware(); 511 | } 512 | 513 | /** 514 | * Sends an error page response. 515 | * 516 | * @param \Lithe\Http\Response $response The response object to send. 517 | * @param int $statusCode The HTTP status code for the error. 518 | * @param string $message The error message to display. 519 | * @return void 520 | */ 521 | private function sendErrorPage(\Lithe\Http\Response $response, int $statusCode, string $message) 522 | { 523 | // Set the HTTP status code and send the HTML error page 524 | $response->status($statusCode)->send('' . $message . '
' . $statusCode . '' . $message . '
'); 525 | } 526 | 527 | /** 528 | * Checks if the URL matches the route pattern and validates parameter types. 529 | * 530 | * @param string $routePattern The route pattern. 531 | * @param string $url The URL to be checked. 532 | * @return bool True if the URL matches the route pattern and parameter types are valid, False otherwise. 533 | * @throws InvalidParameterTypeException If an invalid parameter type is encountered. 534 | */ 535 | private function matchRoute(string $routePattern, string $url): bool 536 | { 537 | // Remove leading slash from route pattern and URL 538 | $routePattern = ltrim($routePattern, '/'); 539 | $url = ltrim($url, '/'); 540 | 541 | // Build regex pattern 542 | $pattern = "#^" . preg_replace_callback('/:(\w+)(?:=([^\/]+))?/', function ($matches) { 543 | $paramName = $matches[1]; 544 | $paramType = $matches[2] ?? 'string'; // Default type is string if not specified 545 | return "(?P<" . $paramName . ">" . $this->getPatternForType($paramType) . ")"; 546 | }, $routePattern) . "$#"; 547 | 548 | // Execute URL matching against the route pattern 549 | if (!preg_match($pattern, $url, $matches)) { 550 | return false; 551 | } 552 | 553 | // Validate parameter types 554 | foreach ($matches as $key => $value) { 555 | if (is_string($key)) { 556 | $expectedTypes = $this->getExpectedType($routePattern, $key); 557 | if (!$this->validateParameterType($value, $expectedTypes)) { 558 | return false; 559 | } 560 | } 561 | } 562 | 563 | // Check for optional parameters in route that were not matched 564 | if (strpos($routePattern, '?') !== false && !empty($matches)) { 565 | return false; 566 | } 567 | 568 | return true; 569 | } 570 | 571 | /** 572 | * Returns the regex pattern for a parameter type specified in the route. 573 | * 574 | * @param string $types Parameter types specified in the route (e.g., 'int|uuid|regex<\d+>'). 575 | * @return string Regex pattern for the parameter type. 576 | */ 577 | private function getPatternForType(string $types): string 578 | { 579 | $patterns = []; 580 | 581 | // Split parameter types using '|', except within 'regex<...>' 582 | $parts = preg_split('/(regex<[^>]+>)/', $types, -1, PREG_SPLIT_DELIM_CAPTURE); 583 | foreach ($parts as $part) { 584 | if (strpos($part, 'regex<') === 0) { 585 | // Handle custom regex patterns inside 'regex<...>' 586 | $regex = substr($part, 6, -1); 587 | $patterns[] = "($regex)"; 588 | } elseif (strpos($part, '|') !== false) { 589 | // Handle standard parameter types separated by '|' 590 | $subParts = explode('|', $part); 591 | foreach ($subParts as $subPart) { 592 | if (!empty(trim($subPart))) { // Check if part is not empty or only contains spaces 593 | $patterns[] = $this->getPatternForStandardType($subPart); 594 | } 595 | } 596 | } elseif (!empty(trim($part))) { // Check if part is not empty or only contains spaces 597 | // Handle standard parameter types 598 | $patterns[] = $this->getPatternForStandardType($part); 599 | } 600 | } 601 | 602 | return implode('|', $patterns); 603 | } 604 | 605 | /** 606 | * Returns the regex pattern for a standard parameter type. 607 | * 608 | * @param string $type Standard parameter type. 609 | * @return string Regex pattern for the parameter type. 610 | * @throws InvalidParameterTypeException If an invalid parameter type is encountered. 611 | */ 612 | private function getPatternForStandardType(string $type): string 613 | { 614 | switch ($type) { 615 | case 'int': 616 | return '[0-9]+'; 617 | case 'string': 618 | return '[^/]+'; 619 | case 'uuid': 620 | return '[a-f\d]{8}(-[a-f\d]{4}){3}[a-f\d]{12}'; 621 | case 'date': 622 | return '\d{4}-\d{1,2}-\d{1,2}'; 623 | case 'email': 624 | return '[^\s@]+@[^\s@]+\.[^\s@]+'; 625 | case 'bool': 626 | return '(false|true|0|1)'; 627 | case 'float': 628 | return '[-+]?[0-9]*\.?[0-9]+'; 629 | case 'slug': 630 | return '[a-z0-9]+(?:-[a-z0-9]+)*'; 631 | case 'username': 632 | return '[a-zA-Z0-9_]{3,20}'; 633 | case 'tel': 634 | return '\+?[\d\-\(\)]+'; 635 | case 'file': 636 | return '[^/]+(?:\.([^.]+))'; 637 | case 'alphanumeric': 638 | return '[a-zA-Z0-9]+'; 639 | default: 640 | throw new InvalidParameterTypeException("Invalid parameter type: $type"); 641 | } 642 | } 643 | 644 | /** 645 | * Returns the expected types for a parameter based on the route pattern. 646 | * 647 | * @param string $routePattern Route pattern. 648 | * @param string $paramName Parameter name. 649 | * @return string Expected types for the parameter. 650 | */ 651 | private function getExpectedType(string $routePattern, string $paramName): string 652 | { 653 | preg_match('/:' . $paramName . '=([^\/]+)/', $routePattern, $matches); 654 | return $matches[1] ?? 'string'; 655 | } 656 | 657 | /** 658 | * Extracts parameters from a URL based on the route pattern. 659 | * 660 | * @param string $routePattern The pattern of the route. 661 | * @param string $url The URL from which parameters will be extracted. 662 | * @return object An object containing the extracted parameters. 663 | * @throws InvalidParameterTypeException If an invalid parameter type is encountered. 664 | */ 665 | private function extractParams(string $routePattern, string $url): object 666 | { 667 | // Anonymous class to store parameters dynamically 668 | $params = new class() 669 | { 670 | /** 671 | * Magic method to retrieve parameter values. 672 | * 673 | * @param string $name The name of the parameter. 674 | * @return mixed|null Returns the parameter value if defined, or null if not found. 675 | */ 676 | public function __get($name) 677 | { 678 | return $this->$name ?? null; 679 | } 680 | }; 681 | 682 | // Remove leading slashes from route pattern and URL 683 | $routePattern = ltrim($routePattern, '/'); 684 | $url = ltrim($url, '/'); 685 | 686 | // Replace ':param=type' syntax with '{param:type}' for regex 687 | $pattern = "#^" . preg_replace_callback('/:(\w+)(?:=([^\/]+))?/', function ($matches) { 688 | $paramName = $matches[1]; 689 | $paramType = $matches[2] ?? 'string'; // Default type is string if not specified 690 | return "(?P<" . $paramName . ">" . $this->getPatternForType($paramType) . ")"; 691 | }, $routePattern) . "$#"; 692 | 693 | // Match the URL against the route pattern 694 | if (!preg_match($pattern, $url, $matches)) { 695 | return $params; // Return an empty object if there's no match 696 | } 697 | 698 | // Extract and convert matched parameters 699 | foreach ($matches as $key => $value) { 700 | if (is_string($key) && !is_numeric($key)) { // Ensure $key is a string and not numeric 701 | $expectedTypes = $this->getExpectedType($routePattern, $key); 702 | $params->$key = $this->convertParameterValue($value, $expectedTypes); 703 | } 704 | } 705 | 706 | return $params; 707 | } 708 | 709 | /** 710 | * Validates the value of a parameter according to the specified types. 711 | * 712 | * @param mixed $value The parameter value. 713 | * @param string $types The types of the parameter. 714 | * @return bool True if the value matches any of the types, False otherwise. 715 | * @throws InvalidParameterTypeException If the parameter type is invalid. 716 | */ 717 | private function validateParameterType($value, string $types): bool 718 | { 719 | // Split types by '|', handling cases inside 'regex<...>' 720 | $typesArray = $this->parseTypes($types); 721 | 722 | foreach ($typesArray as $type) { 723 | // Check if it's a custom regular expression 'regex<...>' 724 | if (strpos($type, 'regex<') === 0 && substr($type, -1) === '>') { 725 | $regexPattern = substr($type, 6, -1); // Get the regex inside regex<...> 726 | if (preg_match("/$regexPattern/", $value) === 1) return true; // Validate the value with regex 727 | } else { 728 | // Handle standard types like 'int', 'float', etc. 729 | switch ($type) { 730 | case 'int': 731 | if (preg_match("#^[0-9]+$#", $value) === 1) return true; 732 | break; 733 | case 'float': 734 | if (is_float($value) || (is_numeric($value) && strpos($value, '.') !== false)) return true; 735 | break; 736 | case 'bool': 737 | if (is_bool($value) || in_array(strtolower($value), ['true', 'false', '1', '0'], true)) return true; 738 | break; 739 | case 'uuid': 740 | if (preg_match('/^[a-f\d]{8}(-[a-f\d]{4}){4}[a-f\d]{8}$/i', $value)) return true; 741 | break; 742 | case 'date': 743 | if (strtotime($value)) return true; 744 | break; 745 | case 'email': 746 | if (filter_var($value, FILTER_VALIDATE_EMAIL) !== false) return true; 747 | break; 748 | case 'slug': 749 | if (preg_match("#^[a-z0-9]+(?:-[a-z0-9]+)*$#", $value) === 1) return true; 750 | break; 751 | case 'username': 752 | if (preg_match("#^[a-zA-Z0-9_]{3,20}$#", $value) === 1) return true; 753 | break; 754 | case 'tel': 755 | if (preg_match("#^\+?[\d\-\(\)]+$#", $value) === 1) return true; 756 | break; 757 | case 'file': 758 | if (preg_match("#^[^/]+(?:\.([^.]+))$#", $value) === 1) return true; 759 | break; 760 | case 'alphanumeric': 761 | if (preg_match("#^[a-zA-Z0-9]+$#", $value) === 1) return true; 762 | break; 763 | case 'string': 764 | return true; 765 | default: 766 | // If no type matches, throw an exception 767 | throw new InvalidParameterTypeException("Invalid parameter type: $type"); 768 | } 769 | } 770 | } 771 | 772 | return false; 773 | } 774 | 775 | /** 776 | * Parses the types string into an array of individual types. 777 | * 778 | * @param string $types The types string to parse (e.g., 'int|uuid|regex<\d+>'). 779 | * @return array An array containing individual types parsed from the string. 780 | */ 781 | private function parseTypes(string $types): array 782 | { 783 | $typesArray = []; 784 | $currentType = ''; 785 | $depth = 0; 786 | 787 | // Iterate through each character in the $types string 788 | for ($i = 0; $i < strlen($types); $i++) { 789 | $char = $types[$i]; 790 | 791 | // Check if a '|' separator is found at the outermost level 792 | if ($char === '|' && $depth === 0) { 793 | $typesArray[] = trim($currentType); // Add the current type to the array 794 | $currentType = ''; // Reset the current type string for the next type 795 | } else { 796 | $currentType .= $char; // Append the character to the current type string 797 | 798 | // Update depth to handle '<' and '>' 799 | if ($char === '<') { 800 | $depth++; 801 | } elseif ($char === '>') { 802 | $depth--; 803 | } 804 | } 805 | } 806 | 807 | // Add the last found type to the array, if any remains 808 | if (!empty($currentType)) { 809 | $typesArray[] = trim($currentType); 810 | } 811 | 812 | return $typesArray; 813 | } 814 | 815 | /** 816 | * Converts the parameter value to the first valid specified type. 817 | * 818 | * @param mixed $value The parameter value. 819 | * @param string $types The types of the parameter. 820 | * @return mixed The converted value to the first valid type. 821 | * @throws InvalidParameterTypeException If the parameter type is invalid. 822 | */ 823 | private function convertParameterValue($value, string $types) 824 | { 825 | foreach (explode('|', $types) as $type) { 826 | try { 827 | switch ($type) { 828 | case 'int': 829 | if (preg_match("#^[0-9]+$#", $value)) return (int) $value; 830 | break; 831 | case 'float': 832 | if (is_numeric($value) && strpos($value, '.') !== false) return (float) $value; 833 | break; 834 | case 'bool': 835 | if (is_bool($value) || in_array(strtolower($value), ['true', 'false', '1', '0'], true)) return filter_var($value, FILTER_VALIDATE_BOOLEAN); 836 | break; 837 | case 'uuid': 838 | if (preg_match('/^[a-f\d]{8}(-[a-f\d]{4}){4}[a-f\d]{8}$/i', $value)) return $value; 839 | break; 840 | case 'date': 841 | if (strtotime($value)) return date('Y-m-d', strtotime($value)); 842 | break; 843 | case 'email': 844 | if (filter_var($value, FILTER_VALIDATE_EMAIL) !== false) return $value; 845 | break; 846 | case 'slug': 847 | if (preg_match("#^[a-z0-9]+(?:-[a-z0-9]+)*$#", $value)) return $value; 848 | break; 849 | case 'username': 850 | if (preg_match("#^[a-zA-Z0-9_]{3,16}$#", $value)) return $value; 851 | break; 852 | case 'tel': 853 | if (preg_match("#^\+?[\d\-\(\)]+$#", $value)) return $value; 854 | break; 855 | case 'file': 856 | if (preg_match("#^[^/]+(?:\.([^.]+))$#", $value)) return $value; 857 | break; 858 | case 'alphanumeric': 859 | if (preg_match("#^[a-zA-Z0-9]+$#", $value)) return $value; 860 | break; 861 | case 'string': 862 | return (string) $value; 863 | } 864 | } catch (\Exception $e) { 865 | // Continue to the next type if conversion fails 866 | } 867 | } 868 | return (string) $value; // Default to string if no types match 869 | } 870 | } 871 | -------------------------------------------------------------------------------- /src/Config/app/engines.php: -------------------------------------------------------------------------------- 1 | function (string $file, string $views, array $data = []) { 7 | // Check if Blade is available 8 | if (class_exists('Jenssegers\Blade\Blade')) { 9 | try { 10 | // Define cache and instantiate Blade 11 | $cache = dirname(__DIR__, 6) . '/storage/framework/views'; 12 | $blade = new \Jenssegers\Blade\Blade($views, $cache); 13 | 14 | // Render and output the Blade view 15 | echo $blade->make($file, $data)->render(); 16 | } catch (Exception $e) { 17 | // Handle rendering errors 18 | $errorMessage = 'Error rendering view with Blade: ' . $e->getMessage(); 19 | error_log($errorMessage); 20 | Log::error($errorMessage); 21 | } 22 | } else { 23 | // Blade not installed, throw exception 24 | $errorMessage = 'Blade is not installed. Install it to use the template feature ( composer require jenssegers/blade ).'; 25 | Log::error($errorMessage); 26 | throw new RuntimeException($errorMessage); 27 | } 28 | }, 29 | 'twig' => function (string $file, string $views, array $data = []) { 30 | // Check if Twig is available 31 | if (class_exists('Twig\Environment')) { 32 | try { 33 | // Set up Twig loader and environment 34 | $loader = new \Twig\Loader\FilesystemLoader($views); 35 | $twig = new \Twig\Environment($loader); 36 | 37 | // Render and output the Twig template 38 | echo $twig->render("$file.twig", $data); 39 | } catch (\Twig\Error\LoaderError | \Twig\Error\RuntimeError | \Twig\Error\SyntaxError $e) { 40 | // Handle Twig rendering errors 41 | $errorMessage = "Error rendering Twig template: " . $e->getMessage(); 42 | error_log($errorMessage); 43 | Log::error($errorMessage); 44 | } 45 | } else { 46 | // Twig not installed, throw exception 47 | $errorMessage = 'Twig is not installed. Install it to use the Twig template feature.'; 48 | Log::error($errorMessage); 49 | throw new RuntimeException($errorMessage); 50 | } 51 | }, 52 | 'default' => function (string $file, string $views, array $data = []) { 53 | // Render using default PHP include 54 | $viewPath = $views . "/$file.php"; 55 | 56 | if (file_exists($viewPath)) { 57 | extract($data); // Extract data for use in view 58 | include $viewPath; // Include PHP view file 59 | } else { 60 | // View file not found, throw exception 61 | $errorMessage = "File not found: $viewPath"; 62 | Log::error($errorMessage); 63 | throw new RuntimeException($errorMessage); 64 | } 65 | }, 66 | 67 | // Add more options as needed 68 | ]; 69 | -------------------------------------------------------------------------------- /src/Config/app/settings.php: -------------------------------------------------------------------------------- 1 | include __DIR__ . '/engines.php', 5 | // Add more configurations as needed 6 | ]; 7 | -------------------------------------------------------------------------------- /src/Config/database.php: -------------------------------------------------------------------------------- 1 | function (object $dbConfig) { 7 | if (class_exists('Illuminate\Database\Capsule\Manager')) { 8 | $capsule = new \Illuminate\Database\Capsule\Manager; 9 | $capsule->addConnection([ 10 | 'driver' => $dbConfig->driver ?? 'mysql', 11 | 'host' => $dbConfig->host ?? '127.0.0.1', 12 | 'database' => $dbConfig->database ?? 'test', 13 | 'username' => $dbConfig->username ?? 'root', 14 | 'password' => $dbConfig->password ?? '', 15 | 'charset' => 'utf8', 16 | 'collation' => 'utf8_unicode_ci', 17 | 'prefix' => '', 18 | ]); 19 | $capsule->setAsGlobal(); 20 | $capsule->bootEloquent(); 21 | return $capsule; 22 | } else { 23 | throw new RuntimeException('Eloquent is not installed.'); 24 | } 25 | }, 26 | 'pdo' => function (object $dbConfig) { 27 | try { 28 | return new PDO( 29 | self::getDsn($dbConfig->host) . ';dbname=' . $dbConfig->database, 30 | $dbConfig->username, 31 | $dbConfig->password 32 | ); 33 | } catch (PDOException $e) { 34 | // Log detailed error 35 | Log::error("Database connection error: " . $e->getMessage()); 36 | 37 | die('An error occurred while connecting to the database.'); 38 | } 39 | }, 40 | 'mysqli' => function (object $dbConfig) { 41 | $mysqli = new \mysqli( 42 | $dbConfig->host, 43 | $dbConfig->username, 44 | $dbConfig->password, 45 | $dbConfig->database 46 | ); 47 | 48 | if ($mysqli->connect_error) { 49 | // Log detailed error 50 | Log::error("Database connection error: " . $mysqli->connect_error); 51 | 52 | die('An error occurred while connecting to the database.'); 53 | } 54 | 55 | return $mysqli; 56 | } 57 | ]; -------------------------------------------------------------------------------- /src/Console/Commands/Controller/make.php: -------------------------------------------------------------------------------- 1 | getFormatter()->setStyle('info-bg', $outputStyle); 20 | 21 | // Get the controller name argument from the input 22 | $name = $input->getArgument('name'); 23 | // Define the path for the new controller file 24 | $controllerPath = PROJECT_ROOT . "/http/controllers/{$name}.php"; 25 | // Define the directory for the new controller file 26 | $controllerDir = dirname($controllerPath); 27 | 28 | // Verify and create the controller directory if it does not exist 29 | verifyAndCreateDirectory($controllerDir, $io); 30 | 31 | // Verify if the controller file already exists 32 | if (controllerExists($controllerPath)) { 33 | $io->warning("The controller already exists in {$controllerPath}."); 34 | return Command::FAILURE; 35 | } 36 | 37 | // Generate the content for the new controller file 38 | $controllerContent = generateControllerContent($name); 39 | // Write the content to the file 40 | file_put_contents($controllerPath, $controllerContent); 41 | 42 | // Output success message 43 | $io->writeln("\n\r INFO Controller [{$controllerPath}] created successfully.\n"); 44 | 45 | return Command::SUCCESS; 46 | }, 47 | [ 48 | // Define the 'name' argument for the command 49 | 'name' => [ 50 | InputArgument::REQUIRED, // The argument is required 51 | 'Controller name' // Description of the argument 52 | ] 53 | ] 54 | ); 55 | 56 | // Function to verify if the controller file already exists 57 | function controllerExists($controllerPath) 58 | { 59 | return file_exists($controllerPath); 60 | } 61 | 62 | // Function to generate the content for the new controller file 63 | function generateControllerContent($name) 64 | { 65 | return <<getFormatter()->setStyle('info-bg', $outputStyle); 20 | 21 | // Define the .env file path 22 | $envFilePath = '.env'; 23 | 24 | if (!file_exists($envFilePath)) { 25 | $io->error(".env file not found!"); 26 | return Command::FAILURE; 27 | } 28 | 29 | // Load the .env file content 30 | $envContent = file_get_contents($envFilePath); 31 | 32 | // Generate a new encryption key 33 | $key = base64_encode(random_bytes(32)); // Use Base64 encoding 34 | 35 | // Check if APP_KEY already exists in the .env file 36 | if (preg_match('/^APP_KEY=.*$/m', $envContent)) { 37 | if (preg_match('/^APP_KEY=\\s*$/m', $envContent)) { 38 | // APP_KEY is empty, so just replace it 39 | $envContent = preg_replace('/^APP_KEY=.*$/m', 'APP_KEY=' . $key, $envContent); 40 | } else { 41 | // APP_KEY is not empty, ask user for confirmation 42 | $io->note("An APP_KEY already exists in the .env file."); 43 | $confirm = $io->confirm("Do you want to overwrite it and generate a new key?", false); 44 | 45 | if (!$confirm) { 46 | $io->success("No changes made. Exiting."); 47 | return Command::SUCCESS; 48 | } 49 | 50 | $envContent = preg_replace('/^APP_KEY=.*$/m', 'APP_KEY=' . $key, $envContent); 51 | } 52 | } else { 53 | // APP_KEY does not exist, append it 54 | $envContent .= "\nAPP_KEY=" . $key; 55 | } 56 | 57 | // Save the updated .env file 58 | file_put_contents($envFilePath, $envContent); 59 | 60 | $io->writeln("\n\r INFO New APP_KEY key generated and set in the .env file.\n"); 61 | 62 | return Command::SUCCESS; 63 | } 64 | ); 65 | -------------------------------------------------------------------------------- /src/Console/Commands/Middleware/make.php: -------------------------------------------------------------------------------- 1 | getFormatter()->setStyle('info-bg', $outputStyle); 21 | 22 | // Get the middleware name argument from the input 23 | $name = $input->getArgument('name'); 24 | // Define the path for the new middleware file 25 | $middlewarePath = PROJECT_ROOT . "/http/middleware/{$name}.php"; 26 | // Define the directory for the new middleware file 27 | $middlewareDir = dirname($middlewarePath); 28 | 29 | // Verify and create the middleware directory if it does not exist 30 | verifyAndCreateDirectory($middlewareDir, $io); 31 | 32 | // Verify if the middleware file already exists 33 | verifyMiddlewareExists($middlewarePath, $io); 34 | 35 | // Generate the content for the new middleware file 36 | $middlewareContent = generateMiddlewareContent($name); 37 | // Write the content to the file 38 | file_put_contents($middlewarePath, $middlewareContent); 39 | 40 | $project_root = PROJECT_ROOT; 41 | 42 | // Output success message 43 | $io->writeln("\n\r INFO Middleware [{$project_root}/Http/Middleware/{$name}.php] created successfully.\n"); 44 | 45 | return Command::SUCCESS; 46 | }, 47 | [ 48 | // Define the 'name' argument for the command 49 | 'name' => [ 50 | InputArgument::REQUIRED, // The argument is required 51 | 'Middleware name' // Description of the argument 52 | ] 53 | ] 54 | ); 55 | 56 | // Function to verify if the middleware file already exists 57 | function verifyMiddlewareExists($middlewarePath, SymfonyStyle $io) 58 | { 59 | if (file_exists($middlewarePath)) { 60 | // Output a warning if the file already exists 61 | $io->warning("The Middleware already exists in {$middlewarePath}."); 62 | // Exit with a failure status 63 | exit(Command::FAILURE); 64 | } 65 | } 66 | 67 | // Function to generate the content for the new middleware file 68 | function generateMiddlewareContent($name) 69 | { 70 | return <<getFormatter()->setStyle('info-bg', $outputStyle); 20 | $io = new SymfonyStyle($input, $output); 21 | 22 | // Define different migration templates based on database connection method 23 | $templates = [ 24 | 'default' => << << << <<getArgument('name'); 131 | // Retrieve the 'template' option from the input or default to the environment variable 132 | $template = $input->getOption('template') ?: (Env::get('DB_CONNECTION_METHOD', 'default')); 133 | 134 | if ($template) { 135 | // Check if the template provided via input is valid 136 | if (!isset($templates[$template])) { 137 | $io->error('Invalid template'); 138 | return Command::FAILURE; 139 | } 140 | } else { 141 | // Try to get the template from the environment variable 142 | $DB_CONNECTION_METHOD = Env::get('DB_CONNECTION_METHOD', 'default'); 143 | 144 | // Check if the template from the environment variable is valid 145 | if (!isset($templates[$DB_CONNECTION_METHOD])) { 146 | // If the template is not valid, set 'default' as fallback 147 | $template = 'default'; 148 | } else { 149 | // Otherwise, use the template from the environment variable 150 | $template = $DB_CONNECTION_METHOD; 151 | } 152 | } 153 | 154 | // Ensure the migration name is provided 155 | if (empty($name)) { 156 | $io->warning('The name of the migration is mandatory.'); 157 | return Command::FAILURE; 158 | } 159 | 160 | // Define the path where migration files will be stored 161 | $path = PROJECT_ROOT . '/database/migrations/'; 162 | // Create the directory if it does not exist 163 | if (!is_dir($path)) { 164 | mkdir($path, 0755, true); 165 | } 166 | 167 | // Generate the migration file content based on the selected template 168 | $content = $templates[$template]; 169 | // Create a unique file name based on the current timestamp and migration name 170 | $filePath = $path . date('Y') . "_" . date('m') . "_" . date('d') . "_" . date('His') . '_' . $name . '.php'; 171 | 172 | // Write the content to the migration file 173 | file_put_contents($filePath, "writeln("\n\r INFO Migration [$filePath] created successfully.\n"); 177 | 178 | return Command::SUCCESS; 179 | }, 180 | [ 181 | // Define the 'name' argument as required for the command 182 | 'name' => [ 183 | InputArgument::REQUIRED, 184 | 'Migration name' 185 | ], 186 | ], 187 | [ 188 | // Define the 'template' option for the command 189 | 'template' => [null, InputOption::VALUE_REQUIRED, 'Template type', ''] 190 | ] 191 | ); 192 | -------------------------------------------------------------------------------- /src/Console/Commands/Migrations/migrate.php: -------------------------------------------------------------------------------- 1 | getFormatter()->setStyle('info-bg', $outputStyle); 23 | 24 | // Ensure the migrations table exists 25 | Migration::createTableIfItDoesntExist(); 26 | 27 | $migrationsDir = PROJECT_ROOT . '/database/migrations'; 28 | 29 | if (is_dir($migrationsDir)) { 30 | $migrations = Migration::all(); 31 | $executedAnyMigration = false; 32 | $lastBatch = Migration::findLastBatch(); 33 | $currentBatch = $lastBatch ? $lastBatch + 1 : 1; 34 | 35 | // Get all migration files, excluding '.' and '..' 36 | $migrationFiles = array_diff(scandir($migrationsDir), ['.', '..']); 37 | 38 | if (count($migrationFiles) === 0) { 39 | $io->writeln("\n\r INFO Nothing to migrate.\n"); 40 | return Command::SUCCESS; 41 | } 42 | 43 | $io->writeln("\n\r INFO Running migrations.\n"); 44 | 45 | foreach ($migrationFiles as $file) { 46 | $filePath = "$migrationsDir/$file"; 47 | 48 | if (shouldMigrate($filePath, $migrations)) { 49 | $executedAnyMigration = executeMigration($filePath, $currentBatch, $io); 50 | } 51 | } 52 | 53 | if (!$executedAnyMigration) { 54 | $io->writeln("\n\r INFO All migrations are up to date.\n"); 55 | } 56 | 57 | $io->newLine(); 58 | } else { 59 | $io->writeln("\n\r INFO Migrations directory not found.\n"); 60 | } 61 | 62 | return Command::SUCCESS; 63 | } catch (Exception $e) { 64 | $io->error($e->getMessage()); 65 | return Command::FAILURE; 66 | } 67 | }, 68 | [], // No arguments 69 | [] // No options 70 | ); 71 | 72 | /** 73 | * Determine if a migration should be executed. 74 | * 75 | * @param string $filePath The path to the migration file. 76 | * @param \Illuminate\Support\Collection $migrations The collection of executed migrations. 77 | * @return bool True if the migration should be executed, False otherwise. 78 | */ 79 | function shouldMigrate(string $filePath, $migrations): bool 80 | { 81 | foreach ($migrations as $migration) { 82 | if ($migration['migration'] === $filePath) { 83 | return false; 84 | } 85 | } 86 | return true; 87 | } 88 | 89 | 90 | /** 91 | * Execute a migration. 92 | * 93 | * @param string $filePath The path to the migration file. 94 | * @param int $batch The batch number for the migration. 95 | * @param SymfonyStyle $io The SymfonyStyle output instance. 96 | * @return bool True if the migration was executed successfully, False otherwise. 97 | */ 98 | function executeMigration(string $filePath, int $batch, SymfonyStyle $io): bool 99 | { 100 | $migrationClass = include $filePath; 101 | 102 | if (!is_object($migrationClass)) { 103 | $io->writeln("Failed to load migration class from '$filePath'."); 104 | return false; 105 | } 106 | 107 | $migrationClass->up(DB::connection()); 108 | 109 | Migration::add($filePath, $batch); 110 | 111 | $io->writeln(sprintf("\r %s .......................................................................................... DONE", basename($filePath))); 112 | 113 | return true; 114 | } 115 | -------------------------------------------------------------------------------- /src/Console/Commands/Migrations/refresh.php: -------------------------------------------------------------------------------- 1 | error($e->getMessage()); 24 | return Command::FAILURE; 25 | } 26 | 27 | // Create a custom style for the "INFO" message with a blue background 28 | $infoStyle = new OutputFormatterStyle('white', 'blue'); 29 | $output->getFormatter()->setStyle('info-bg', $infoStyle); 30 | 31 | // Retrieve all migrations in descending order 32 | $migrations = Migration::getOrdered('id', 'DESC'); 33 | $executedAnyRollback = false; 34 | 35 | // Check if the migrations directory exists 36 | if (is_dir(PROJECT_ROOT . '/database/migrations')) { 37 | // If there are no migrations to rollback 38 | if (empty($migrations)) { 39 | $io->writeln("\n\r INFO Nothing to rollback.\n"); 40 | return Command::SUCCESS; 41 | } else { 42 | // Notify that rollback of migrations is starting 43 | $io->writeln("\n\r INFO Rolling back all migrations.\n"); 44 | 45 | // Rollback each migration 46 | foreach ($migrations as $migration) { 47 | $executedAnyRollback = rollbackMigration($migration, $io) || $executedAnyRollback; 48 | } 49 | } 50 | } else { 51 | // If the migrations directory does not exist, output an error 52 | $io->writeln("\n\r INFO Migrations directory not found.\n"); 53 | return Command::FAILURE; 54 | } 55 | 56 | // If no rollback was executed, notify the user 57 | if (!$executedAnyRollback) { 58 | $io->writeln("\n\r INFO Nothing to rollback.\n"); 59 | return Command::SUCCESS; 60 | } 61 | 62 | // Run the migrations again 63 | runMigrations($io); 64 | 65 | return Command::SUCCESS; 66 | }, 67 | [], // No arguments 68 | [] // No options 69 | ); 70 | 71 | /** 72 | * Runs all the database migrations. 73 | * 74 | * @param SymfonyStyle $io The SymfonyStyle for console output. 75 | */ 76 | function runMigrations(SymfonyStyle $io): void 77 | { 78 | $dir = PROJECT_ROOT . '/database/migrations'; 79 | $files = array_diff(scandir($dir), array('.', '..')); 80 | $batch = 1; 81 | 82 | if (count($files) === 0) { 83 | $io->writeln("\n\r INFO No migrations to run.\n"); 84 | return; 85 | } 86 | 87 | $io->writeln("\n\r INFO Running migrations.\n"); 88 | 89 | foreach ($files as $file) { 90 | $path = "$dir/$file"; 91 | $migrationClass = include $path; 92 | 93 | if (!is_object($migrationClass)) { 94 | $io->writeln("Failed to load migration class from '$path'."); 95 | continue; 96 | } 97 | 98 | $migrationClass->up(DB::connection()); 99 | 100 | Migration::add($path, $batch); 101 | 102 | $io->writeln(sprintf("\r %s .......................................................................................... DONE", basename($path))); 103 | } 104 | 105 | $io->newLine(); 106 | } 107 | -------------------------------------------------------------------------------- /src/Console/Commands/Migrations/reset.php: -------------------------------------------------------------------------------- 1 | error($e->getMessage()); 23 | return Command::FAILURE; 24 | } 25 | 26 | // Create a custom style for the "INFO" message with a blue background 27 | $infoStyle = new OutputFormatterStyle('white', 'blue'); 28 | $output->getFormatter()->setStyle('info-bg', $infoStyle); 29 | 30 | // Retrieve all migrations in descending order 31 | $migrations = Migration::getOrdered('id', 'DESC'); 32 | $executedAnyRollback = false; 33 | 34 | // Check if there are no migrations to rollback 35 | if (empty($migrations)) { 36 | $io->writeln("\n\r INFO Nothing to rollback.\n"); 37 | return Command::SUCCESS; 38 | } 39 | 40 | $io->writeln("\n\r INFO Rolling back all migrations.\n"); 41 | 42 | // Rollback each migration 43 | foreach ($migrations as $migration) { 44 | $executedAnyRollback = rollbackMigration($migration, $io) || $executedAnyRollback; 45 | } 46 | 47 | // If no rollback was executed, inform the user 48 | if (!$executedAnyRollback) { 49 | $io->writeln("\n\r INFO Nothing to rollback.\n"); 50 | } else { 51 | $io->newLine(); 52 | } 53 | 54 | return Command::SUCCESS; 55 | }, 56 | // Arguments and options can be passed here, if needed 57 | [], 58 | [] 59 | ); 60 | -------------------------------------------------------------------------------- /src/Console/Commands/Migrations/rollback.php: -------------------------------------------------------------------------------- 1 | error($e->getMessage()); 25 | return Command::FAILURE; 26 | } 27 | 28 | // Create a custom style for the "INFO" message with a blue background 29 | $infoStyle = new OutputFormatterStyle('white', 'blue'); 30 | $output->getFormatter()->setStyle('info-bg', $infoStyle); 31 | 32 | // Retrieve the options from the input 33 | $batchOption = $input->getOption('batch'); 34 | $forceOption = $input->getOption('force'); 35 | 36 | // Check if the force option is enabled and if in production 37 | if ($forceOption && $this->isProduction()) { 38 | $io->warning('You are in production mode. Using the --force option is risky.'); 39 | if (!$io->confirm('Do you really want to proceed?', false)) { 40 | return Command::SUCCESS; 41 | } 42 | } 43 | 44 | // Find the batch to rollback to 45 | $batch = $batchOption ?? Migration::findLastBatch(); 46 | 47 | if ($batch === false) { 48 | $io->writeln("\n\r INFO No migrations found.\n"); 49 | return Command::SUCCESS; 50 | } 51 | 52 | $io->writeln("\n\r INFO Rolling back migrations up to batch $batch.\n"); 53 | 54 | // Retrieve the migrations to rollback 55 | $migrations = Migration::getWhere('batch', $batch); 56 | $executedAnyRollback = false; 57 | 58 | // Rollback each migration 59 | foreach ($migrations as $migration) { 60 | $executedAnyRollback = rollbackMigration($migration, $io) || $executedAnyRollback; 61 | } 62 | 63 | // Notify if no migrations were rolled back 64 | if (!$executedAnyRollback) { 65 | $io->writeln("\n\r INFO No migrations to rollback.\n"); 66 | } else { 67 | $io->newLine(); 68 | } 69 | 70 | return Command::SUCCESS; 71 | }, 72 | [], 73 | [ 74 | 'batch' => ['b', InputOption::VALUE_OPTIONAL, 'Rollback migrations up to a specific batch number'], 75 | 'force' => ['f', InputOption::VALUE_NONE, 'Force the operation to run when in production'] 76 | ] 77 | ); 78 | 79 | // Define the isProduction method in the scope of the anonymous function 80 | function isProduction(): bool 81 | { 82 | return Env::get('APP_PRODUCTION_MODE', false); 83 | } 84 | -------------------------------------------------------------------------------- /src/Console/Commands/Models/make.php: -------------------------------------------------------------------------------- 1 | getArgument('name'); 22 | // Define the path where the new model file will be created 23 | $modelPath = PROJECT_ROOT . "/models/{$name}.php"; 24 | // Define the directory for the new model file 25 | $modelDir = dirname($modelPath); 26 | // Get the specified template option, if any 27 | $template = $input->getOption('template'); 28 | 29 | // Create a custom style for informational messages with a blue background 30 | $outputStyle = new OutputFormatterStyle('white', 'blue'); 31 | $output->getFormatter()->setStyle('info-bg', $outputStyle); 32 | 33 | // Define available templates for the model 34 | $templates = [ 35 | 'default' => [ 36 | 'use' => 'use Lithe\Database\Manager as DB;', 37 | 'extends' => '' 38 | ], 39 | 'eloquent' => [ 40 | 'use' => 'use Illuminate\Database\Eloquent\Model;', 41 | 'extends' => 'extends Model' 42 | ], 43 | ]; 44 | 45 | if ($template) { 46 | // Check if the template provided via input is valid 47 | if (!isset($templates[$template])) { 48 | $io->error('Invalid template'); 49 | return Command::FAILURE; 50 | } 51 | } else { 52 | // Try to get the template from the environment variable 53 | $DB_CONNECTION_METHOD = Env::get('DB_CONNECTION_METHOD', 'default'); 54 | 55 | // Check if the template from the environment variable is valid 56 | if (!isset($templates[$DB_CONNECTION_METHOD])) { 57 | // If the template is not valid, set 'default' as fallback 58 | $template = 'default'; 59 | } else { 60 | // Otherwise, use the template from the environment variable 61 | $template = $DB_CONNECTION_METHOD; 62 | } 63 | } 64 | 65 | // Verify and create the directory if it does not exist 66 | verifyAndCreateDirectory($modelDir, $io); 67 | 68 | // Check if the model file already exists 69 | if (file_exists($modelPath)) { 70 | $io->warning("The model already exists in {$modelPath}."); 71 | return Command::FAILURE; 72 | } 73 | 74 | // Generate the content for the new model file 75 | $modelContent = <<writeln("\n\r INFO Model [{$project_root}/models/{$name}.php] created successfully.\n"); 92 | 93 | return Command::SUCCESS; 94 | }, 95 | [ 96 | // Define the 'name' argument for the command 97 | 'name' => [ 98 | InputArgument::REQUIRED, // Argument is required 99 | 'Model name' // Description of the argument 100 | ] 101 | ], 102 | [ 103 | // Define the 'template' option for the command 104 | 'template' => [ 105 | null, // No short option 106 | InputOption::VALUE_REQUIRED, // Option requires a value 107 | 'Template type', // Description of the option 108 | null // Default value if not provided 109 | ] 110 | ] 111 | ); 112 | -------------------------------------------------------------------------------- /src/Console/Commands/Seeders/make.php: -------------------------------------------------------------------------------- 1 | getFormatter()->setStyle('info-bg', $outputStyle); 20 | $io = new SymfonyStyle($input, $output); 21 | 22 | // Define different seeder templates based on database connection method 23 | $templates = [ 24 | 'default' => << << <<getArgument('name'); 76 | 77 | // Validate the seeder name 78 | if (!preg_match('/^[a-zA-Z0-9_]+$/', $name)) { 79 | $io->error('The seeder name must contain only alphanumeric characters and underscores.'); 80 | return Command::FAILURE; 81 | } 82 | 83 | // Get the specified template option, if any 84 | $template = $input->getOption('template'); 85 | 86 | if ($template) { 87 | // Check if the template provided via input is valid 88 | if (!isset($templates[$template])) { 89 | $io->error('Invalid template'); 90 | return Command::FAILURE; 91 | } 92 | } else { 93 | // Try to get the template from the environment variable 94 | $DB_CONNECTION_METHOD = Env::get('DB_CONNECTION_METHOD', 'default'); 95 | 96 | // Check if the template from the environment variable is valid 97 | if (!isset($templates[$DB_CONNECTION_METHOD])) { 98 | // If the template is not valid, set 'default' as fallback 99 | $template = 'default'; 100 | } else { 101 | // Otherwise, use the template from the environment variable 102 | $template = $DB_CONNECTION_METHOD; 103 | } 104 | } 105 | 106 | // Define the path where seeder files will be stored 107 | $path = PROJECT_ROOT . '/database/seeders/'; 108 | // Create the directory if it does not exist 109 | if (!is_dir($path)) { 110 | mkdir($path, 0755, true); 111 | } 112 | 113 | // Generate the seeder file content based on the selected template 114 | $content = str_replace('{className}', $name, $templates[$template]); 115 | // Create the file path using the seeder name 116 | $filePath = $path . $name . '.php'; 117 | 118 | // Write the content to the seeder file 119 | file_put_contents($filePath, $content); 120 | 121 | // Output a success message indicating the seeder file was created 122 | $io->writeln("\n\r INFO Seeder [$filePath] created successfully. You can now run it using your seeding logic.\n"); 123 | 124 | return Command::SUCCESS; 125 | }, 126 | [ 127 | // Define the 'name' argument as required for the command 128 | 'name' => [ 129 | InputArgument::REQUIRED, 130 | 'Seeder name' 131 | ], 132 | ], 133 | [ 134 | // Define the 'template' option for the command 135 | 'template' => [null, InputOption::VALUE_REQUIRED, 'Template type', ''] 136 | ] 137 | ); 138 | -------------------------------------------------------------------------------- /src/Console/Commands/Seeders/seed.php: -------------------------------------------------------------------------------- 1 | getFormatter()->setStyle('info-bg', $outputStyle); 18 | $io = new SymfonyStyle($input, $output); 19 | 20 | // Retrieve the 'class' option from the input 21 | $class = $input->getOption('class'); 22 | 23 | // Define the path where seeder files are stored 24 | $seedersPath = PROJECT_ROOT . '/database/seeders/'; 25 | 26 | // If a class is specified, run that specific seeder 27 | if ($class) { 28 | // Check if the seeder class file exists 29 | $filePath = $seedersPath . $class . '.php'; 30 | if (!file_exists($filePath)) { 31 | $io->error("Seeder class [$class] does not exist."); 32 | return Command::FAILURE; 33 | } 34 | 35 | // Include the seeder class 36 | require_once $filePath; 37 | 38 | // Create an instance of the seeder class and run it 39 | $seederInstance = new $class(); 40 | if (method_exists($seederInstance, 'run')) { 41 | $seederInstance->run(Manager::connection()); 42 | $io->writeln("\n\r INFO Seeder $class executed successfully.\n"); 43 | } else { 44 | $io->error("Seeder class [$class] must have a run() method."); 45 | return Command::FAILURE; 46 | } 47 | } else { 48 | // If no class is specified, run all seeders in the directory 49 | $files = glob($seedersPath . '*.php'); 50 | 51 | foreach ($files as $file) { 52 | // Get the class name from the file name 53 | $fileName = basename($file, '.php'); 54 | // Include the seeder class 55 | require_once $file; 56 | 57 | // Create an instance of the seeder class and run it 58 | if (class_exists($fileName)) { 59 | $seederInstance = new $fileName(); 60 | if (method_exists($seederInstance, 'run')) { 61 | $seederInstance->run(Manager::connection()); 62 | $io->writeln("\n\r INFO Seeder $fileName executed successfully.\n"); 63 | } else { 64 | $io->error("Seeder class [$fileName] must have a run() method."); 65 | } 66 | } else { 67 | $io->error("Seeder class [$fileName] does not exist."); 68 | } 69 | } 70 | } 71 | 72 | return Command::SUCCESS; 73 | }, 74 | [], 75 | [ 76 | // Define the 'class' option for the command 77 | 'class' => [ 78 | null, 79 | InputOption::VALUE_REQUIRED, 80 | 'The seeder class to execute', 81 | ], 82 | ] 83 | ); 84 | -------------------------------------------------------------------------------- /src/Console/Commands/Server/serve.php: -------------------------------------------------------------------------------- 1 | getFormatter()->setStyle('info-bg', $outputStyle); 21 | 22 | // Retrieve the 'port' argument from the input 23 | $PORT = $input->getArgument('port'); 24 | // Display the server start message with the port number 25 | $io->writeln("\n\r INFO Server started on http://localhost:$PORT\n"); 26 | // Execute the shell command to start the PHP built-in server on the specified port 27 | shell_exec("php -S localhost:$PORT -t public"); 28 | return Command::SUCCESS; 29 | }, 30 | [ 31 | // Define the 'port' argument for the command 32 | 'port' => [ 33 | InputArgument::OPTIONAL, // Argument is optional 34 | 'Specifies the port for the server (default is 8000)', // Description of the argument 35 | '8000' // Default value for the port if not provided 36 | ] 37 | ] 38 | ); 39 | -------------------------------------------------------------------------------- /src/Console/Line.php: -------------------------------------------------------------------------------- 1 | get(); 35 | 36 | self::use([ 37 | include __DIR__ . '/Commands/Server/serve.php', 38 | include __DIR__ . '/Commands/Models/make.php', 39 | include __DIR__ . '/Commands/Middleware/make.php', 40 | include __DIR__ . '/Commands/Controller/make.php', 41 | include __DIR__ . '/Commands/Keys/generate.php', 42 | include __DIR__ . '/Commands/Migrations/make.php', 43 | include __DIR__ . '/Commands/Migrations/migrate.php', 44 | include __DIR__ . '/Commands/Migrations/refresh.php', 45 | include __DIR__ . '/Commands/Migrations/reset.php', 46 | include __DIR__ . '/Commands/Migrations/rollback.php', 47 | include __DIR__ . '/Commands/Seeders/make.php', 48 | include __DIR__ . '/Commands/Seeders/seed.php', 49 | ]); 50 | } 51 | 52 | /** 53 | * Registers multiple commands. 54 | * 55 | * @param SymfonyCommand[] $commands Array of SymfonyCommand objects. 56 | * @throws InvalidArgumentException If any item is not an instance of SymfonyCommand. 57 | */ 58 | public static function use(array $commands): void 59 | { 60 | self::initialize(); 61 | 62 | foreach ($commands as $command) { 63 | if ($command instanceof SymfonyCommand) { 64 | self::$application->add($command); 65 | } else { 66 | throw new InvalidArgumentException('All items in the array must be instances of SymfonyCommand.'); 67 | } 68 | } 69 | } 70 | 71 | /** 72 | * Runs the console application. 73 | * 74 | * @param array $args Command line arguments 75 | * @return int 76 | */ 77 | public static function listen(array $args = []): int 78 | { 79 | self::initialize(); 80 | self::loadDefaultCommands(); 81 | 82 | try { 83 | return self::$application->run(new \Symfony\Component\Console\Input\ArgvInput($args)); 84 | } catch (\Exception $e) { 85 | // You might want to log the exception or handle it in another way 86 | echo 'Error: ' . $e->getMessage(); 87 | return 1; 88 | } 89 | } 90 | 91 | /** 92 | * Creates a new command instance. 93 | * 94 | * @param string $name Command name 95 | * @param string $description Command description 96 | * @param callable $handler Command handler 97 | * @param array $arguments Command arguments 98 | * @param array $options Command options 99 | * @return SymfonyCommand 100 | */ 101 | public static function create( 102 | string $name, 103 | string $description, 104 | callable $handler, 105 | array $arguments = [], 106 | array $options = [] 107 | ): SymfonyCommand { 108 | return new class($name, $description, $handler, $arguments, $options) extends SymfonyCommand 109 | { 110 | private $handler; 111 | 112 | public function __construct( 113 | string $name, 114 | string $description, 115 | callable $handler, 116 | array $arguments = [], 117 | array $options = [] 118 | ) { 119 | parent::__construct($name); 120 | $this->setDescription($description); 121 | $this->handler = $handler; 122 | 123 | foreach ($arguments as $argName => $argOptions) { 124 | $this->addArgument($argName, ...$argOptions); 125 | } 126 | 127 | foreach ($options as $optName => $optOptions) { 128 | $this->addOption($optName, ...$optOptions); 129 | } 130 | } 131 | 132 | protected function execute(InputInterface $input, OutputInterface $output): int 133 | { 134 | return (int) call_user_func($this->handler, $input, $output); 135 | } 136 | }; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Console/Parts/Migrations/model.php: -------------------------------------------------------------------------------- 1 | exec($query); 50 | } catch (PDOException $e) { 51 | throw new RuntimeException("Failed to create migrations table: " . $e->getMessage()); 52 | } 53 | } 54 | 55 | /** 56 | * Find the last batch of migrations. 57 | * 58 | * @return int|bool The last batch number, or false if not found 59 | */ 60 | public static function findLastBatch() 61 | { 62 | try { 63 | $pdo = self::getPdo(); 64 | $stmt = $pdo->query("SELECT batch FROM " . self::$table . " ORDER BY id DESC LIMIT 1"); 65 | $result = $stmt->fetch(PDO::FETCH_ASSOC); 66 | return $result ? $result['batch'] : false; 67 | } catch (\Exception $e) { 68 | error_log($e->getMessage()); 69 | return false; 70 | } 71 | } 72 | 73 | /** 74 | * Add a new migration. 75 | * 76 | * @param string $migration The migration content 77 | * @param int $batch The batch number 78 | * @return bool True on success, false on failure 79 | */ 80 | public static function add($migration, $batch) 81 | { 82 | try { 83 | $pdo = self::getPdo(); 84 | $stmt = $pdo->prepare("INSERT INTO " . self::$table . " (migration, batch) VALUES (:migration, :batch)"); 85 | $stmt->bindParam(':migration', $migration); 86 | $stmt->bindParam(':batch', $batch, PDO::PARAM_INT); 87 | return $stmt->execute(); 88 | } catch (\Exception $e) { 89 | error_log($e->getMessage()); 90 | return false; 91 | } 92 | } 93 | 94 | /** 95 | * Delete a migration by ID. 96 | * 97 | * @param int $id The migration ID 98 | * @return bool True on success, false on failure 99 | */ 100 | public static function delete($id) 101 | { 102 | try { 103 | $pdo = self::getPdo(); 104 | $stmt = $pdo->prepare("DELETE FROM " . self::$table . " WHERE id = :id"); 105 | $stmt->bindParam(':id', $id, PDO::PARAM_INT); 106 | return $stmt->execute(); 107 | } catch (\Exception $e) { 108 | error_log($e->getMessage()); 109 | return false; 110 | } 111 | } 112 | 113 | /** 114 | * Save a migration (update if exists, insert if not). 115 | * 116 | * @param int $id The migration ID 117 | * @param string $migration The migration content 118 | * @param int $batch The batch number 119 | * @return bool True on success, false on failure 120 | */ 121 | public static function save($id, $migration, $batch) 122 | { 123 | try { 124 | $pdo = self::getPdo(); 125 | $stmt = $pdo->prepare("SELECT COUNT(*) FROM " . self::$table . " WHERE id = :id"); 126 | $stmt->bindParam(':id', $id, PDO::PARAM_INT); 127 | $stmt->execute(); 128 | $exists = $stmt->fetchColumn() > 0; 129 | 130 | if ($exists) { 131 | $stmt = $pdo->prepare("UPDATE " . self::$table . " SET migration = :migration, batch = :batch WHERE id = :id"); 132 | } else { 133 | $stmt = $pdo->prepare("INSERT INTO " . self::$table . " (id, migration, batch) VALUES (:id, :migration, :batch)"); 134 | } 135 | 136 | $stmt->bindParam(':id', $id, PDO::PARAM_INT); 137 | $stmt->bindParam(':migration', $migration); 138 | $stmt->bindParam(':batch', $batch, PDO::PARAM_INT); 139 | return $stmt->execute(); 140 | } catch (\Exception $e) { 141 | error_log($e->getMessage()); 142 | return false; 143 | } 144 | } 145 | 146 | /** 147 | * Retrieve migrations with a WHERE clause. 148 | * 149 | * @param string $column The column name 150 | * @param mixed $value The value to match 151 | * @return array|bool Array of migrations, or false on failure 152 | */ 153 | public static function getWhere($column, $value) 154 | { 155 | try { 156 | $pdo = self::getPdo(); 157 | $stmt = $pdo->prepare("SELECT * FROM " . self::$table . " WHERE {$column} = :value ORDER BY id DESC"); 158 | $stmt->bindParam(':value', $value); 159 | $stmt->execute(); 160 | return $stmt->fetchAll(PDO::FETCH_ASSOC); 161 | } catch (\Exception $e) { 162 | error_log($e->getMessage()); 163 | return false; 164 | } 165 | } 166 | 167 | /** 168 | * Retrieve migrations ordered by a specific column. 169 | * 170 | * @param string $column The column name 171 | * @param string $direction The sorting direction (default: 'ASC') 172 | * @return array|bool Array of migrations, or false on failure 173 | */ 174 | public static function getOrdered($column, $direction = 'ASC') 175 | { 176 | try { 177 | $pdo = self::getPdo(); 178 | $stmt = $pdo->prepare("SELECT * FROM " . self::$table . " ORDER BY {$column} {$direction}"); 179 | $stmt->execute(); 180 | return $stmt->fetchAll(PDO::FETCH_ASSOC); 181 | } catch (\Exception $e) { 182 | error_log($e->getMessage()); 183 | return false; 184 | } 185 | } 186 | 187 | /** 188 | * Retrieve all migrations. 189 | * 190 | * @return array|bool Array of migrations, or false on failure 191 | */ 192 | public static function all() 193 | { 194 | try { 195 | $pdo = self::getPdo(); 196 | $stmt = $pdo->query("SELECT * FROM " . self::$table); 197 | return $stmt->fetchAll(PDO::FETCH_ASSOC); 198 | } catch (\Exception $e) { 199 | error_log($e->getMessage()); 200 | return false; 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/Console/Parts/Migrations/rollback.php: -------------------------------------------------------------------------------- 1 | writeln(sprintf("\r %s .......................................................................................... Migration not found", basename($path))); 12 | Migration::delete($migration['id']); 13 | return false; 14 | } 15 | 16 | $migrationClass = include $path; 17 | 18 | // Verifica se o arquivo incluído retorna um objeto válido 19 | if (!is_object($migrationClass)) { 20 | $io->writeln("Failed to load migration class from '$path'."); 21 | return false; 22 | } 23 | 24 | // Executa o método down da classe de migração e exclui o registro da migração 25 | $migrationClass->down(DB::connection()); 26 | Migration::delete($migration['id']); 27 | 28 | $io->writeln(sprintf("\r %s .......................................................................................... DONE", basename($path))); 29 | 30 | return true; 31 | } -------------------------------------------------------------------------------- /src/Console/Parts/verifyAndCreateDirectory.php.php: -------------------------------------------------------------------------------- 1 | host), $dbConfig->username, $dbConfig->password); 36 | $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 37 | 38 | $stmt = $pdo->prepare("SHOW DATABASES LIKE ?"); 39 | $stmt->execute([$dbConfig->database]); 40 | 41 | if (!$stmt->fetch()) { 42 | $pdo->exec("CREATE DATABASE `{$dbConfig->database}` CHARACTER SET utf8 COLLATE utf8_unicode_ci"); 43 | } 44 | } catch (PDOException $e) { 45 | // Log detailed error 46 | Log::error("Database connection error: " . $e->getMessage()); 47 | 48 | die('An error occurred while connecting to the database.'); 49 | } 50 | } 51 | 52 | /** 53 | * Returns the DSN for PDO connection. 54 | * 55 | * @param string $dbHost The database host. 56 | * @return string 57 | */ 58 | private static function getDsn(string $dbHost) 59 | { 60 | $database = Env::get(self::DB_CONNECTION, 'mysql'); 61 | return "$database:host=$dbHost"; 62 | } 63 | 64 | /** 65 | * Returns the default settings for database connections. 66 | * 67 | * @return array 68 | */ 69 | private static function defaultSettings() 70 | { 71 | return include __DIR__ . '/../Config/database.php'; 72 | } 73 | 74 | /** 75 | * Configures a database connection. 76 | * 77 | * @param string $name The name of the connection. 78 | * @param callable $config The configuration function for the connection. 79 | */ 80 | public static function configure(string $name, callable $config) 81 | { 82 | self::$settings[$name] = $config; 83 | } 84 | 85 | /** 86 | * Initializes and returns the configured database connection. 87 | * 88 | * @param string|null $name The name of the database configuration to initialize. 89 | * @return mixed The result of the database connection initialization. 90 | * @throws RuntimeException If there is an error setting up the connection. 91 | * @throws Exception If the specified database configuration is not found. 92 | */ 93 | public static function initialize(string $name = null) 94 | { 95 | try { 96 | $requiredEnvVariables = [ 97 | self::DB_CONNECTION_METHOD, 98 | self::DB_CONNECTION, 99 | self::DB_HOST, 100 | self::DB_NAME, 101 | self::DB_USERNAME, 102 | self::DB_PASSWORD, 103 | self::DB_SHOULD_INITIATE 104 | ]; 105 | 106 | foreach ($requiredEnvVariables as $envVariable) { 107 | if (!Env::has($envVariable)) { 108 | throw new RuntimeException("Missing environment variable: $envVariable"); 109 | } 110 | } 111 | 112 | if (!filter_var(Env::get(self::DB_SHOULD_INITIATE), FILTER_VALIDATE_BOOLEAN)) { 113 | return null; // Return null if initialization is not required 114 | } 115 | 116 | if (!$name) { 117 | $name = Env::get(self::DB_CONNECTION_METHOD); 118 | } 119 | 120 | $settings = array_merge(self::defaultSettings(), self::$settings); 121 | 122 | if (isset($settings[$name])) { 123 | $dbConfig = (object) [ 124 | 'driver' => Env::get(self::DB_CONNECTION, 'mysql'), 125 | 'host' => Env::get(self::DB_HOST, '127.0.0.1'), 126 | 'database' => Env::get(self::DB_NAME, 'test'), 127 | 'username' => Env::get(self::DB_USERNAME, 'root'), 128 | 'password' => Env::get(self::DB_PASSWORD, '') 129 | ]; 130 | 131 | // Check if we are in production mode 132 | if (!filter_var(Env::get('APP_PRODUCTION_MODE', false), FILTER_VALIDATE_BOOLEAN)) { 133 | self::createDatabaseIfNotExists($dbConfig); 134 | } 135 | 136 | return self::setupConnection($name, $dbConfig, $settings); 137 | } else { 138 | throw new Exception("Database configuration '$name' not found."); 139 | } 140 | } catch (Exception $e) { 141 | Log::error("Error initializing the database: " . $e->getMessage()); 142 | die("An error occurred while initializing the database: " . $e->getMessage()); 143 | } 144 | } 145 | 146 | /** 147 | * Establishes and configures the database connection using the specified settings and configuration. 148 | * 149 | * This method retrieves the appropriate connection configuration by name and attempts to establish 150 | * a connection to the database. If successful, the connection is stored for later use. 151 | * 152 | * @param string $name The name of the database connection to set up. 153 | * @param object $dbConfig An object containing the database configuration parameters such as host, 154 | * username, password, and database name. 155 | * @param array $settings An associative array containing callable configuration functions for database connections. 156 | * 157 | * @return mixed Returns the established database connection on success. 158 | * 159 | * @throws RuntimeException If an error occurs while setting up the database connection. 160 | */ 161 | private static function setupConnection(string $name, object $dbConfig, array $settings) 162 | { 163 | try { 164 | // Retrieve the connection using the specified settings 165 | $connection = $settings[$name]($dbConfig); 166 | 167 | // Store the connection for future use 168 | self::$connection[$name] = $connection; 169 | 170 | return $connection; 171 | } catch (Exception $e) { 172 | Log::error("Error setting up the '$name' connection: " . $e->getMessage()); 173 | throw new RuntimeException('An error occurred while setting up the database connection: ' . $e->getMessage()); 174 | } 175 | } 176 | 177 | /** 178 | * Retrieves the current database connection instance based on the specified method. 179 | * 180 | * @param string|null $method The method to use for the database connection (e.g., 'pdo', 'mysqli'). 181 | * @return mixed|null The current database connection instance, or null if it is not initialized. 182 | * @throws InvalidArgumentException If the specified connection method is not supported. 183 | */ 184 | public static function connection(?string $method = null) 185 | { 186 | // Se o método não for fornecido, pega o método da variável de ambiente 187 | if ($method === null) { 188 | $method = Env::get('DB_CONNECTION_METHOD'); 189 | } 190 | 191 | // Verifica se o método de conexão está definido no ambiente 192 | if (!isset(self::$connection[$method])) { 193 | throw new \InvalidArgumentException("The connection method '{$method}' is not supported."); 194 | } 195 | 196 | return self::$connection[$method]; 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidParameterTypeException.php: -------------------------------------------------------------------------------- 1 | method = $this->method(); 69 | 70 | /** 71 | * Extract request headers. 72 | */ 73 | $this->headers = $this->extractHeaders(); 74 | 75 | /** 76 | * Extract cookies from the request. 77 | */ 78 | $this->cookies = $this->extractCookies(); 79 | 80 | $this->parameters = $parameters; 81 | 82 | $this->params = $parameters; 83 | 84 | // Get the client's IP address 85 | $ip = $_SERVER['REMOTE_ADDR']; 86 | if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { 87 | $ip = $_SERVER['HTTP_X_FORWARDED_FOR']; 88 | } 89 | 90 | $this->ip = $ip; 91 | 92 | /** 93 | * Extract query string data from the URL. 94 | */ 95 | $this->query = $this->extractQuery(); 96 | 97 | /** 98 | * Extract body data from the request. 99 | */ 100 | $this->body = $this->extractBody(); 101 | 102 | /** 103 | * Extract uploaded files from the request. 104 | */ 105 | $this->files = $this->extractFiles(); 106 | 107 | /** 108 | * Get the clean URL path. 109 | */ 110 | $this->url = $url; 111 | 112 | $this->path = $url; 113 | } 114 | 115 | /** 116 | * Get a property value. 117 | * 118 | * This magic method allows dynamic access to properties that do not exist in the object. 119 | * 120 | * @param string $name The name of the property. 121 | * @return mixed|null The value of the property if it exists, or null if it does not. 122 | */ 123 | public function __get(string $name) 124 | { 125 | return $this->properties[$name] ?? null; 126 | } 127 | 128 | /** 129 | * Set a property value. 130 | * 131 | * This magic method allows dynamic assignment of properties that do not exist in the object. 132 | * 133 | * @param string $name The name of the property. 134 | * @param mixed $value The value to set for the property. 135 | */ 136 | public function __set(string $name, $value) 137 | { 138 | $this->properties[$name] = $value; 139 | } 140 | 141 | 142 | /** 143 | * Extracts headers from the request. 144 | * 145 | * This function attempts to retrieve all HTTP headers from the request. If the `getallheaders` function is available, 146 | * it uses it to get the headers. Otherwise, it falls back to manually parsing `$_SERVER` to construct the headers array. 147 | * 148 | * @return array An associative array of headers, where the keys are header names and the values are header values. 149 | */ 150 | private function extractHeaders(): array 151 | { 152 | if (function_exists('getallheaders')) { 153 | // Retrieve headers using the getallheaders function if it exists 154 | $headers = getallheaders(); 155 | } else { 156 | // Alternative method to retrieve headers in environments where getallheaders() is not available 157 | $headers = []; 158 | foreach ($_SERVER as $name => $value) { 159 | // Check if the server variable represents an HTTP header 160 | if (substr($name, 0, 5) == 'HTTP_') { 161 | // Format the header name to be in a more readable format (e.g., 'CONTENT_TYPE' to 'Content-Type') 162 | $headerName = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5))))); 163 | $headers[$headerName] = $value; 164 | } 165 | } 166 | } 167 | 168 | return $headers; 169 | } 170 | 171 | /** 172 | * Retrieves the host of the server. 173 | * 174 | * Constructs the host URL considering the server's protocol and host. 175 | * 176 | * @return string The host URL. 177 | */ 178 | function getHost(): string 179 | { 180 | $scheme = $this->protocol() . '://'; 181 | $host = $_SERVER['HTTP_HOST']; 182 | 183 | return $scheme . $host; 184 | } 185 | 186 | /** 187 | * Extracts cookies from the request and returns them as an object. 188 | * 189 | * @return object An object containing cookie values. 190 | */ 191 | private function extractCookies() 192 | { 193 | $cookies = new class 194 | { 195 | 196 | /** 197 | * Get the value of a cookie. 198 | * 199 | * @param string $name The name of the cookie. 200 | * @return mixed|null The value of the cookie if exists, otherwise null. 201 | */ 202 | public function __get(string $name) 203 | { 204 | return $this->$name ?? null; 205 | } 206 | 207 | /** 208 | * Check if a cookie exists. 209 | * 210 | * @param string $name The name of the cookie. 211 | * @return bool Returns true if the cookie exists, false otherwise. 212 | */ 213 | public function exists(string $name): bool 214 | { 215 | return isset($this->$name); 216 | } 217 | }; 218 | 219 | // Populate the $cookies object with existing $_COOKIE values 220 | foreach ($_COOKIE as $name => $value) { 221 | if (is_string($name)) { 222 | $cookies->$name = $value; 223 | } 224 | } 225 | 226 | return $cookies; 227 | } 228 | 229 | /** 230 | * Get the value of a specific cookie. 231 | * 232 | * @param string $name The name of the cookie. 233 | * @param mixed $default Default value to return if the cookie does not exist. 234 | * @return mixed The value of the cookie if it exists, otherwise the default value. 235 | */ 236 | public function cookie(string $name, $default = null) 237 | { 238 | $cookies = $this->extractCookies(); 239 | return $cookies->$name ?? $default; 240 | } 241 | 242 | /** 243 | * Extracts uploaded files information from the $_FILES superglobal. 244 | * 245 | * This method processes the $_FILES superglobal to create an object that contains 246 | * information about uploaded files. It maps each file input name to its corresponding 247 | * file information (e.g., name, type, size, tmp_name, error). 248 | * 249 | * @return object|null Returns an object containing uploaded files information, 250 | * or null if no files are found or an error occurs. 251 | */ 252 | private function extractFiles() 253 | { 254 | // Anonymous class to store files information dynamically 255 | $files = new class 256 | { 257 | /** 258 | * Magic method to retrieve file information by name. 259 | * 260 | * @param string $name The name of the file input. 261 | * @return mixed|null Returns the file information if available, or null if not found. 262 | */ 263 | public function __get($name) 264 | { 265 | return $this->$name ?? null; 266 | } 267 | }; 268 | 269 | try { 270 | // Check if $_FILES contains any data 271 | if (empty($_FILES)) { 272 | return null; 273 | } 274 | 275 | // Iterate through each file in $_FILES 276 | foreach ($_FILES as $key => $fileInfo) { 277 | if (is_array($fileInfo['name'])) { 278 | // Initialize array to store multiple files 279 | $filesArray = []; 280 | foreach ($fileInfo['name'] as $index => $fileName) { 281 | $fileData = [ 282 | 'name' => $fileName, 283 | 'type' => $fileInfo['type'][$index], 284 | 'tmp_name' => $fileInfo['tmp_name'][$index], 285 | 'error' => $fileInfo['error'][$index], 286 | 'size' => $fileInfo['size'][$index], 287 | ]; 288 | $filesArray[] = new \Lithe\Base\Upload($fileData); 289 | } 290 | $files->$key = $filesArray; // Assign array of files 291 | } else { 292 | // Handle single file 293 | $files->$key = new \Lithe\Base\Upload($fileInfo); 294 | } 295 | } 296 | 297 | // Return the object containing uploaded files information 298 | return $files; 299 | } catch (\Exception $e) { 300 | // Error handling: log the error 301 | \error_log("Error extracting uploaded files: " . $e->getMessage()); 302 | 303 | Log::error($e); 304 | 305 | // Return null on error 306 | return $files; 307 | } 308 | } 309 | 310 | /** 311 | * Retrieves the value of a specific header from the request. 312 | * 313 | * @param string $name The name of the desired header. 314 | * @param mixed $default The default value to return if the header does not exist. 315 | * @return mixed The value of the header if it exists, or the default value if it does not. 316 | */ 317 | public function header(string $name, mixed $default = null): mixed 318 | { 319 | $headers = $this->extractHeaders(); 320 | return $headers[$name] ?? $default; 321 | } 322 | 323 | public function isAjax() 324 | { 325 | return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest'; 326 | } 327 | 328 | /** 329 | * Extracts query parameters from the current request URI. 330 | * 331 | * @return object|null Returns an object containing query parameters if available, or null on failure. 332 | */ 333 | private function extractQuery() 334 | { 335 | try { 336 | // Anonymous class to store query parameters dynamically 337 | $query = new class() 338 | { 339 | 340 | /** 341 | * Magic method to retrieve query parameter values. 342 | * 343 | * @param string $name The name of the query parameter. 344 | * @return mixed|null Returns the value of the query parameter if set, or null if not found. 345 | */ 346 | public function __get($name) 347 | { 348 | return $this->$name ?? null; 349 | } 350 | }; 351 | 352 | // Check if 'REQUEST_URI' is defined in $_SERVER 353 | if (isset($_SERVER['REQUEST_URI'])) { 354 | $urlParts = parse_url($_SERVER['REQUEST_URI']); 355 | 356 | // Check if 'query' key is present in URL parts 357 | if (isset($urlParts['query'])) { 358 | // Parse query string into an array of parameters 359 | parse_str($urlParts['query'], $queryData); 360 | 361 | // Iterate over query parameters and add them to the $query object 362 | foreach ($queryData as $key => $value) { 363 | if (is_string($key)) { 364 | $query->$key = $value; 365 | } 366 | } 367 | } 368 | } 369 | 370 | return $query; 371 | } catch (\Exception $e) { 372 | // Error handling: log the error 373 | \error_log("Error extracting query parameters: " . $e->getMessage()); 374 | Log::error($e); 375 | 376 | // Return null on error 377 | return null; 378 | } 379 | } 380 | 381 | /** 382 | * Gets a query parameter from the URL. 383 | * 384 | * @param string $key The name of the query parameter. 385 | * @param mixed $default The default value to return if the query parameter does not exist. 386 | * @return mixed The value of the query parameter if it exists, or the default value if it doesn't. 387 | */ 388 | public function query(string $key = null, $default = null) 389 | { 390 | // Use the extractQuery method to get query parameters 391 | $query = $this->extractQuery(); 392 | 393 | // Return the specific query parameter if a key is provided, or the entire query object if no key is provided 394 | return $key === null ? $query : ($query->$key ?? $default); 395 | } 396 | 397 | /* Get information about an uploaded file. 398 | * 399 | * @param string $name The name of the file input. 400 | * @return \Lithe\Base\Upload|null|array Returns the file information if available, or null if not found. 401 | */ 402 | public function file(string $name) 403 | { 404 | // Use the extractFiles method to get uploaded file information 405 | $files = $this->extractFiles(); 406 | 407 | // Return the specific file information if a name is provided, or the entire files object if no name is provided 408 | return $files->$name; 409 | } 410 | 411 | /** 412 | * Extracts the body of the request, decoding JSON data if present. 413 | * 414 | * @return object|mixed|null An object containing the request data, or the raw value if it's a primitive type, or null if an error occurs. 415 | */ 416 | private function extractBody() 417 | { 418 | try { 419 | // Attempt to fetch JSON data from the request body 420 | $json = file_get_contents("php://input"); 421 | $bodyData = $json !== false ? json_decode($json, true) : null; 422 | 423 | // Check if $bodyData is an array; otherwise, set it as an empty array 424 | if (is_array($bodyData)) { 425 | // If $bodyData is an array, merge it with $_GET and $_POST 426 | $requestData = array_merge($_GET, $_POST, $bodyData); 427 | } elseif ($bodyData !== null) { 428 | // If $bodyData is a non-null primitive value, merge it into an array 429 | $requestData = $bodyData; 430 | } else { 431 | // If $bodyData is null, use an empty array 432 | $requestData = array_merge($_GET, $_POST); 433 | } 434 | 435 | // Convert $requestData to an object if it's an array 436 | if (is_array($requestData)) { 437 | $body = new class 438 | { 439 | public function __get($name) 440 | { 441 | return $this->$name ?? null; 442 | } 443 | }; 444 | foreach ($requestData as $key => $value) { 445 | $body->$key = $value; 446 | } 447 | return $body; 448 | } else { 449 | // If $requestData is a primitive type, return it directly 450 | return $requestData; 451 | } 452 | } catch (\Exception $e) { 453 | // Error handling: log the error 454 | \error_log("Error extracting request body: " . $e->getMessage()); 455 | Log::error("Error extracting request body: " . $e->getMessage()); 456 | return null; 457 | } 458 | } 459 | 460 | /** 461 | * Filters a value based on the specified type. 462 | * 463 | * @param string $key The key that holds the value to be filtered. 464 | * @param string $filterType The type of filter to be applied. 465 | * @param mixed $default The default value to return if the filtering fails or the value is not set. 466 | * @return mixed The filtered value, or the default value if the filter is not supported or the value is invalid. 467 | */ 468 | public function filter(string $key, string $filterType, $default = null) 469 | { 470 | // Retrieve the input value by the given key 471 | $value = $this->input($key); 472 | 473 | if ($value === null) { 474 | return $default; 475 | } 476 | 477 | $filteredValue = false; 478 | 479 | // Apply the appropriate filter based on the filter type 480 | switch ($filterType) { 481 | case 'string': 482 | // Sanitize string using FILTER_SANITIZE_FULL_SPECIAL_CHARS 483 | $filteredValue = filter_var($value, FILTER_SANITIZE_FULL_SPECIAL_CHARS); 484 | break; 485 | case 'email': 486 | // Validate email 487 | $filteredValue = filter_var($value, FILTER_VALIDATE_EMAIL); 488 | break; 489 | case 'int': 490 | // Validate integer 491 | $filteredValue = filter_var($value, FILTER_VALIDATE_INT); 492 | break; 493 | case 'float': 494 | // Validate float 495 | $filteredValue = filter_var($value, FILTER_VALIDATE_FLOAT); 496 | break; 497 | case 'url': 498 | // Validate URL 499 | $filteredValue = filter_var($value, FILTER_VALIDATE_URL); 500 | break; 501 | case 'ip': 502 | // Validate IP address 503 | $filteredValue = filter_var($value, FILTER_VALIDATE_IP); 504 | break; 505 | case 'bool': 506 | // Validate boolean 507 | $filteredValue = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); 508 | break; 509 | case 'alnum': 510 | // Validate alphanumeric 511 | $filteredValue = ctype_alnum($value) ? $value : false; 512 | break; 513 | case 'html': 514 | // Sanitize HTML special characters 515 | $filteredValue = htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); 516 | break; 517 | case 'name': 518 | // Validate name (only letters, spaces, and apostrophes) 519 | $filteredValue = preg_match("#^[a-zA-ZÀ-ú\s']+$#", $value) ? $value : false; 520 | break; 521 | case 'date': 522 | // Validate date 523 | $filteredValue = (bool)strtotime($value) ? $value : false; 524 | break; 525 | case 'datetime': 526 | // Validate datetime 527 | $filteredValue = DateTime::createFromFormat('Y-m-d H:i:s', $value) !== false ? $value : false; 528 | break; 529 | case 'regex': 530 | // Validate with custom regex pattern 531 | $pattern = func_get_arg(3); // Pass the regex pattern as the fourth argument 532 | $filteredValue = preg_match($pattern, $value) ? $value : false; 533 | break; 534 | case 'username': 535 | // Validate username 536 | $filteredValue = preg_match('/^[a-zA-Z0-9_]+$/', $value) ? $value : false; 537 | break; 538 | case 'password': 539 | // Example password validation (at least 8 characters, including at least one number and one special character) 540 | $filteredValue = preg_match('/^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/', $value) ? $value : false; 541 | break; 542 | case 'phone': 543 | // Validate phone number 544 | $filteredValue = preg_match('/^\+?[0-9]{10,15}$/', $value) ? $value : false; 545 | break; 546 | case 'creditcard': 547 | // Validate credit card number 548 | $filteredValue = preg_match('/^\d{16}$/', $value) && $this->luhnCheck($value) ? $value : false; 549 | break; 550 | case 'json': 551 | // Validate JSON 552 | json_decode($value); 553 | $filteredValue = (json_last_error() == JSON_ERROR_NONE) ? $value : false; 554 | break; 555 | case 'uuid': 556 | // Validate UUID 557 | $filteredValue = preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $value) ? $value : false; 558 | break; 559 | default: 560 | // Apply default filter 561 | $filteredValue = filter_var($value, FILTER_DEFAULT); 562 | break; 563 | } 564 | 565 | return $filteredValue !== false ? $filteredValue : $default; 566 | } 567 | 568 | /** 569 | * Luhn algorithm to validate credit card numbers. 570 | * 571 | * @param string $number The credit card number. 572 | * @return bool Whether the number is valid according to the Luhn algorithm. 573 | */ 574 | private function luhnCheck($number) 575 | { 576 | $digits = str_split($number); 577 | $sum = 0; 578 | $alternate = false; 579 | 580 | for ($i = count($digits) - 1; $i >= 0; $i--) { 581 | $n = $digits[$i]; 582 | if ($alternate) { 583 | $n *= 2; 584 | if ($n > 9) { 585 | $n -= 9; 586 | } 587 | } 588 | $sum += $n; 589 | $alternate = !$alternate; 590 | } 591 | 592 | return ($sum % 10 == 0); 593 | } 594 | 595 | /** 596 | * Checks if the current URL matches the specified pattern. 597 | * 598 | * @param string $url The URL pattern to compare against the current URL. 599 | * @return bool Returns true if the current URL matches the pattern, otherwise false. 600 | */ 601 | public function is(string $url): bool 602 | { 603 | // Remove leading and trailing slashes for comparison 604 | $url = trim($url, '/'); 605 | $currentUrl = trim($this->path, '/'); 606 | 607 | // Check for exact match 608 | if ($url === $currentUrl) { 609 | return true; 610 | } 611 | 612 | // Convert wildcard pattern (*) to regex (.*) without escaping it 613 | $regexPattern = '/^' . str_replace('\*', '.*', preg_quote($url, '/')) . '$/i'; 614 | 615 | // Check regex match 616 | return preg_match($regexPattern, $currentUrl) === 1; 617 | } 618 | 619 | /** 620 | * Validates input data against the provided rules. 621 | * 622 | * This method collects relevant input data using the specified rules 623 | * and returns a validator instance with the provided data and rules for further processing. 624 | * 625 | * @param array $rules An associative array where the key is the field name and the value is the validation rule. 626 | * @return \Lithe\Base\Validator Returns a validator instance configured with the provided data and rules. 627 | */ 628 | public function validate(array $rules) 629 | { 630 | // Retrieve input data based on the provided rules 631 | foreach ($rules as $key => $rule) { 632 | $value = $this->input($key); 633 | $data[$key] = $value; 634 | } 635 | 636 | return new \Lithe\Base\Validator($data, $rules); 637 | } 638 | 639 | /** 640 | * Retrieves the entire request body or specific parts of it. 641 | * 642 | * @param array|null $keys An array of keys to retrieve specific parts of the body. If null, returns the entire body. 643 | * @param array|null $exclude An array of keys to exclude from the returned body. 644 | * @return mixed An associative array or an object containing the filtered request body data. 645 | */ 646 | public function body(array $keys = null, array $exclude = null): mixed 647 | { 648 | // Extract the body data 649 | $bodyData = $this->extractBody(); 650 | 651 | // Convert bodyData to an array if it's an object 652 | if (is_object($bodyData)) { 653 | $bodyData = get_object_vars($bodyData); 654 | } 655 | 656 | // Filter the body data based on $keys and $exclude 657 | if (is_array($bodyData)) { 658 | if ($keys !== null) { 659 | // Return only the specified keys 660 | $bodyData = array_intersect_key($bodyData, array_flip($keys)); 661 | } 662 | if ($exclude !== null) { 663 | // Exclude the specified keys 664 | $bodyData = array_diff_key($bodyData, array_flip($exclude)); 665 | } 666 | } 667 | 668 | // Return the filtered body data as an object if needed 669 | if (is_array($bodyData)) { 670 | $body = new \stdClass(); 671 | foreach ($bodyData as $key => $value) { 672 | $body->$key = $value; 673 | } 674 | return $body; 675 | } 676 | 677 | return $bodyData; 678 | } 679 | 680 | /** 681 | * Retrieves a specific input value from the request body or query parameters. 682 | * 683 | * @param string $key The key of the input value to retrieve. 684 | * @param mixed $default The default value to return if the key does not exist. 685 | * @return mixed The value of the input if it exists, or the default value if it doesn't. 686 | */ 687 | public function input(string $key, $default = null) 688 | { 689 | // Extract the body data 690 | $body = $this->extractBody(); 691 | $query = $this->extractQuery(); 692 | 693 | // Check if $body is an array and the key exists 694 | if (is_array($body) && array_key_exists($key, $body)) { 695 | return $body[$key]; 696 | } 697 | 698 | // Check if $body is an object and the property exists 699 | if (is_object($body) && property_exists($body, $key)) { 700 | return $body->$key; 701 | } 702 | 703 | // Check if $query is an object and the property exists 704 | if (is_object($query) && property_exists($query, $key)) { 705 | return $query->$key; 706 | } 707 | 708 | // Return the default value if the key does not exist 709 | return $default; 710 | } 711 | 712 | 713 | /** 714 | * Checks if a specific input field or fields are present in the request data. 715 | * 716 | * @param string|array $key The key or an array of keys to check in the request data. 717 | * @return bool True if the input field(s) are present, otherwise false. 718 | */ 719 | public function has(string|array $key): bool 720 | { 721 | if (is_array($key) && !empty($key)) { 722 | foreach ($key as $k) { 723 | if (!$this->input($k)) { 724 | return false; 725 | } 726 | } 727 | return true; 728 | } 729 | 730 | if (is_string($key)) { 731 | return !!$this->input($key); 732 | } 733 | 734 | return false; 735 | } 736 | 737 | 738 | /** 739 | * Checks if the HTTP request method matches the given method. 740 | * 741 | * @param string $method 742 | * @return bool 743 | */ 744 | public function isMethod(string $method): bool 745 | { 746 | return strtolower($this->method()) === strtolower($method); 747 | } 748 | 749 | /** 750 | * Checks if the request expects a JSON response. 751 | * 752 | * @return bool 753 | */ 754 | public function wantsJson(): bool 755 | { 756 | return strpos($this->header('Accept'), 'application/json') !== false; 757 | } 758 | 759 | /** 760 | * Check if the request is secure (HTTPS). 761 | * 762 | * @return bool 763 | */ 764 | public function secure(): bool 765 | { 766 | return (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || 767 | $_SERVER['SERVER_PORT'] == 443 || 768 | (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https');; 769 | } 770 | 771 | /** 772 | * Get the protocol of the request. 773 | * 774 | * @return string 775 | */ 776 | public function protocol(): string 777 | { 778 | return $this->secure() ? 'https' : 'http'; 779 | } 780 | 781 | /** 782 | * Retrieves the value of a specified parameter by its name. 783 | * 784 | * This method checks if the requested parameter exists and returns its value. 785 | * If the parameter is not found, it returns the provided default value. 786 | * 787 | * @param string $name The name of the parameter to retrieve. 788 | * @param mixed $default The default value to return if the parameter is not found (default: null). 789 | * @return mixed The value of the parameter, or the default value if the parameter is not found. 790 | */ 791 | public function param(string $name, mixed $default = null): mixed 792 | { 793 | // Retrieve the parameter value or default if it doesn't exist 794 | $value = $this->parameters->{$name} ?? $default; 795 | 796 | // If the value exists and is not null, URL-decode it 797 | if ($value !== null) { 798 | $value = urldecode($value); 799 | } 800 | 801 | // Return the value (it can be a string, int, float, boolean, etc.) 802 | return $value; 803 | } 804 | 805 | 806 | /** 807 | * Retrieves the HTTP request method (GET, POST, PUT, DELETE, etc.). 808 | * 809 | * This method returns the request method as a string. If the request method cannot be determined, 810 | * it defaults to 'GET'. 811 | * 812 | * @return string The HTTP request method, or 'GET' if not determined. 813 | */ 814 | public function method(): string 815 | { 816 | // Return the request method from the server variables, or 'GET' if not set 817 | return $_SERVER['REQUEST_METHOD'] ?? 'GET'; 818 | } 819 | }; 820 | -------------------------------------------------------------------------------- /src/Http/Response.php: -------------------------------------------------------------------------------- 1 | Settings = $Settings; 16 | $this->Options = $Options; 17 | } 18 | 19 | /** 20 | * Renders a view. 21 | * 22 | * @param string $file Name of the view file. 23 | * @param array|null $data Data to be passed to the view. 24 | * @throws \InvalidArgumentException If the view engine is not configured correctly. 25 | */ 26 | public function render(string $file, ?array $data = []): void 27 | { 28 | $viewEngine = $this->Options['view engine'] ?? null; 29 | $views = $this->Options['views'] ?? null; 30 | 31 | // Check if view engine and views are configured 32 | if ($viewEngine === null || $views === null) { 33 | throw new \InvalidArgumentException("View configurations are not properly defined."); 34 | } 35 | 36 | // Check if the view engine is configured and callable 37 | if (isset($this->Settings['view engine'][$viewEngine]) && is_callable($this->Settings['view engine'][$viewEngine])) { 38 | $this->Settings['view engine'][$viewEngine]($file, $views, $data); 39 | } else { 40 | throw new \InvalidArgumentException("The view engine '$viewEngine' is not configured correctly."); 41 | } 42 | $this->end(); 43 | } 44 | 45 | /** 46 | * Returns the current HTTP status code of the response. 47 | * 48 | * @return int|null Current HTTP status code. 49 | */ 50 | public function getStatusCode(): ?int 51 | { 52 | return $this->statusCode; // Returns the current HTTP status code of the response 53 | } 54 | 55 | /** 56 | * Renders a view. 57 | * 58 | * @param string $file Name of the view file. 59 | * @param array|null $data Data to be passed to the view. 60 | */ 61 | public function view(string $file, ?array $data = []): void 62 | { 63 | $file = str_replace('.', '/', $file); 64 | $this->render($file, $data); 65 | } 66 | 67 | /** 68 | * Sends a response, which can be serialized JSON data. 69 | * 70 | * @param mixed $data Data to be sent as response. 71 | */ 72 | public function send(mixed $data): void 73 | { 74 | if (is_array($data) || is_object($data)) { 75 | $this->json($data); 76 | } else { 77 | echo $data; 78 | } 79 | $this->end(); 80 | } 81 | 82 | /** 83 | * Redirects to a location using an HTTP redirect. 84 | * 85 | * @param string $url URL to redirect to. 86 | * @param bool $permanent Is this a permanent redirect? (default is false). 87 | * @return void 88 | */ 89 | public function redirect(string $url, bool $permanent = false): void 90 | { 91 | $code = $permanent ? 301 : 302; 92 | $this->status($code); 93 | $this->setHeader("Location", $url); 94 | $this->end(); 95 | } 96 | 97 | /** 98 | * Sends a response in JSON format. 99 | * 100 | * @param mixed $data Data to be sent as JSON response. 101 | */ 102 | public function json(mixed $data): void 103 | { 104 | $this->type("application/json; charset=utf-8"); 105 | echo json_encode($data, JSON_UNESCAPED_UNICODE); 106 | $this->end(); 107 | } 108 | 109 | /** 110 | * Sets the HTTP status code for the response. 111 | * 112 | * @param int $statusCode HTTP status code. 113 | * @return \Lithe\Http\Response Current Response object for chaining. 114 | */ 115 | public function status(int $statusCode): self 116 | { 117 | http_response_code($statusCode); // Sets the new status code 118 | $this->statusCode = $statusCode; 119 | return $this; 120 | } 121 | 122 | /** 123 | * Sets an HTTP header in the response. 124 | * 125 | * @param string $name Name of the header. 126 | * @param string|null $value Value of the header. 127 | * @return \Lithe\Http\Response Current Response object for chaining. 128 | */ 129 | public function setHeader(string $name, ?string $value = null): self 130 | { 131 | if ($value === null) { 132 | header($name); 133 | } else { 134 | header("$name: $value"); 135 | } 136 | return $this; 137 | } 138 | 139 | /** 140 | * Ends the response by sending headers and status code, then exiting the script. 141 | * 142 | * @param string|null $message Optional message to send before ending. 143 | */ 144 | public function end(?string $message = null): void 145 | { 146 | if ($message) { 147 | $this->send($message); 148 | } 149 | exit; 150 | } 151 | 152 | /** 153 | * Sends a file for download. 154 | * 155 | * @param string $file Path to the file. 156 | * @param string|null $name Name of the file for download. 157 | * @param array $headers Additional headers. 158 | * @throws \Lithe\Exceptions\Http\HttpException If the file is not found (404 Not Found). 159 | * @return void 160 | */ 161 | public function download(string $file, ?string $name = null, array $headers = []): void 162 | { 163 | // Check if the file exists 164 | if (!file_exists($file)) { 165 | throw new \Lithe\Exceptions\Http\HttpException(404, 'File not found'); 166 | } 167 | 168 | // Set the name for the download (if not provided, use the base name of the file) 169 | $name = $name ?: basename($file); 170 | 171 | // Set HTTP headers for file download 172 | $this->setHeaders(array_merge([ 173 | 'Content-Description' => 'File Transfer', 174 | 'Content-Type' => 'application/octet-stream', 175 | 'Content-Disposition' => 'attachment; filename="' . $name . '"', 176 | 'Expires' => '0', 177 | 'Cache-Control' => 'must-revalidate', 178 | 'Pragma' => 'public', 179 | 'Content-Length' => filesize($file), 180 | ], $headers)); 181 | 182 | // Send the file content to the client 183 | readfile($file); 184 | 185 | // End the response 186 | $this->end(); 187 | } 188 | 189 | /** 190 | * Sets multiple headers at once. 191 | * 192 | * @param array $headers Associative array of headers. 193 | * @return self Current Response object for chaining. 194 | */ 195 | public function setHeaders(array $headers): self 196 | { 197 | foreach ($headers as $name => $value) { 198 | $this->setHeader($name, $value); 199 | } 200 | return $this; 201 | } 202 | 203 | /** 204 | * Displays a file in the browser. 205 | * 206 | * @param string $file Path to the file. 207 | * @param array $headers Additional headers. 208 | * @throws \Lithe\Exceptions\Http\HttpException If the file is not found (404 Not Found). 209 | * @return void 210 | */ 211 | public function file(string $file, array $headers = []): void 212 | { 213 | // Check if the file exists 214 | if (!file_exists($file)) { 215 | throw new \Lithe\Exceptions\Http\HttpException(404, 'File not found'); 216 | } 217 | 218 | // Set HTTP headers for displaying the file 219 | $this->setHeaders(array_merge([ 220 | 'Content-Type' => mime_content_type($file), 221 | 'Content-Length' => filesize($file), 222 | ], $headers)); 223 | 224 | // Send the file content to the client 225 | readfile($file); 226 | 227 | // End the response 228 | $this->end(); 229 | } 230 | 231 | /** 232 | * Sets a new cookie. 233 | * 234 | * @param string $name The name of the cookie. 235 | * @param mixed $value The value of the cookie. 236 | * @param array $options Options to configure the cookie (default: []). 237 | * - 'expire' (int): Expiration time of the cookie in seconds from the current time (default: 0). 238 | * - 'path' (string): Path on the server where the cookie will be available (default: '/'). 239 | * - 'domain' (string): The domain for which the cookie is available (default: null). 240 | * - 'secure' (bool): Indicates if the cookie should be transmitted only over a secure HTTPS connection (default: false). 241 | * - 'httponly' (bool): When true, the cookie can only be accessed through HTTP protocol (default: true). 242 | * @return \Lithe\Http\Response Returns the Response object for method chaining. 243 | * @throws \RuntimeException If headers have already been sent. 244 | */ 245 | public function cookie(string $name, $value, array $options = []): \Lithe\Http\Response 246 | { 247 | // Validate parameters 248 | if (empty($name) || !is_string($name)) { 249 | throw new \InvalidArgumentException('Cookie name must be a non-empty string.'); 250 | } 251 | 252 | // Ensure value is a string or can be converted to a string 253 | if (!is_scalar($value) && !is_null($value)) { 254 | throw new \InvalidArgumentException('Cookie value must be a scalar or null.'); 255 | } 256 | 257 | // Default options for the cookie 258 | $defaults = [ 259 | 'expire' => 0, 260 | 'path' => '/', 261 | 'domain' => null, 262 | 'secure' => false, 263 | 'httponly' => true, 264 | ]; 265 | 266 | // Merge the provided options with the default options 267 | $options = array_merge($defaults, $options); 268 | 269 | if (isset($options['expire']) && !is_int($options['expire'])) { 270 | $options['expire'] = strtotime($options['expire']); 271 | } 272 | 273 | // Check if headers have been sent 274 | if (headers_sent()) { 275 | throw new \RuntimeException('Cannot set cookie. Headers have already been sent.'); 276 | } 277 | 278 | // Set the cookie 279 | setcookie($name, (string)$value, $options['expire'], $options['path'], $options['domain'], $options['secure'], $options['httponly']); 280 | 281 | return $this; 282 | } 283 | 284 | /** 285 | * Remove a cookie. 286 | * 287 | * @param string $name The name of the cookie to be removed. 288 | * @return \Lithe\Http\Response 289 | */ 290 | public function clearCookie(string $name): \Lithe\Http\Response 291 | { 292 | // If the cookie does not exist, there's no need to remove it 293 | if (!isset($_COOKIE[$name])) { 294 | return $this; 295 | } 296 | 297 | // Set the cookie with an expiration time in the past to remove it 298 | $this->cookie($name, '', ['expire' => time() - 3600]); 299 | 300 | // Unset the cookie from the $_COOKIE superglobal to ensure it is removed immediately 301 | unset($_COOKIE[$name]); 302 | 303 | return $this; 304 | } 305 | 306 | /** 307 | * Sets the MIME type for the response. 308 | * 309 | * @param string $mimeType The MIME type to set for the response. 310 | * @return self The current Response object for method chaining. 311 | */ 312 | public function type(string $mimeType): self 313 | { 314 | // Set the 'Content-Type' header with the specified MIME type 315 | $this->setHeader('Content-Type', $mimeType); 316 | 317 | // Return the current instance to allow method chaining 318 | return $this; 319 | } 320 | }; 321 | -------------------------------------------------------------------------------- /src/Http/Router.php: -------------------------------------------------------------------------------- 1 | addRoute('GET', $path, $handler); 35 | } 36 | 37 | /** 38 | * Adds a route for handling POST requests. 39 | * 40 | * @param string $path The route path. 41 | * @param callable ...$handler The handlers (callbacks) for the route. 42 | */ 43 | public function post(string $path, callable|array ...$handler): void 44 | { 45 | $this->addRoute('POST', $path, $handler); 46 | } 47 | 48 | /** 49 | * Adds a route for handling PUT requests. 50 | * 51 | * @param string $path The route path. 52 | * @param callable ...$handler The handlers (callbacks) for the route. 53 | */ 54 | public function put(string $path, callable|array ...$handler): void 55 | { 56 | $this->addRoute('PUT', $path, $handler); 57 | } 58 | 59 | /** 60 | * Adds a route for handling DELETE requests. 61 | * 62 | * @param string $path The route path. 63 | * @param callable ...$handler The handlers (callbacks) for the route. 64 | */ 65 | public function delete(string $path, callable|array ...$handler): void 66 | { 67 | $this->addRoute('DELETE', $path, $handler); 68 | } 69 | 70 | /** 71 | * Adds a route for handling PATCH requests. 72 | * 73 | * @param string $path The route path. 74 | * @param callable ...$handler The handlers (callbacks) for the route. 75 | */ 76 | public function patch(string $path, callable|array ...$handler): void 77 | { 78 | $this->addRoute('PATCH', $path, $handler); 79 | } 80 | 81 | /** 82 | * Adds a route for handling OPTIONS requests. 83 | * 84 | * @param string $path The route path. 85 | * @param callable ...$handler The handlers (callbacks) for the route. 86 | */ 87 | public function options(string $path, callable|array ...$handler): void 88 | { 89 | $this->addRoute('OPTIONS', $path, $handler); 90 | } 91 | 92 | /** 93 | * Adds a route for handling HEAD requests. 94 | * 95 | * @param string $path The route path. 96 | * @param callable ...$handler The handlers (callbacks) for the route. 97 | */ 98 | public function head(string $path, callable|array ...$handler): void 99 | { 100 | $this->addRoute('HEAD', $path, $handler); 101 | } 102 | 103 | /** 104 | * Adds a route to the routes array, checking if it already exists. 105 | * 106 | * @param string $method The HTTP method for the route. 107 | * @param string $path The route path. 108 | * @param callable|array $handler The handlers (callbacks) for the route. 109 | */ 110 | protected function addRoute(string $method, string $path, callable|array $handler): void 111 | { 112 | // Check if the route already exists 113 | foreach ($this->routes as &$route) { 114 | if ($route['method'] === $method && $route['route'] === $path) { 115 | // Route already exists, just add the handler 116 | $route['handler'] = array_merge($route['handler'], $handler); 117 | return; 118 | } 119 | } 120 | 121 | // If the route does not exist, add it to the routes array 122 | $this->routes[] = [ 123 | 'method' => $method, 124 | 'route' => $path, 125 | 'handler' => $handler, 126 | ]; 127 | } 128 | 129 | /** 130 | * Adds a middleware or a router to the application. 131 | * 132 | * @param string|callable|Router|array ...$middleware The middlewares or routers to be added. 133 | */ 134 | public function use(string|callable|Router|array ...$middleware): void 135 | { 136 | // Check if the first parameter is a string (route prefix) 137 | $prefix = null; 138 | if (isset($middleware[0]) && is_string($middleware[0]) && strpos($middleware[0], '/') === 0) { 139 | $prefix = array_shift($middleware); // Remove the route prefix from the middleware array 140 | } 141 | 142 | foreach ($middleware as $mid) { 143 | if ($mid instanceof Router) { 144 | $this->addRouter($mid, $prefix ?: ''); 145 | } elseif (is_callable($mid) || is_array($mid)) { 146 | // Add the middleware to the application's middleware list 147 | $this->middlewares[] = $mid; 148 | } 149 | } 150 | } 151 | 152 | /** 153 | * Adds a Router as middleware for a group of routes. 154 | * 155 | * @param Router $router The Router instance to be added. 156 | * @param string $routePattern The route pattern for the route group. 157 | */ 158 | protected function addRouter(Router $router, string $routePattern): void 159 | { 160 | foreach ($router->routes as $route) { 161 | $path = $route['route'] === '/' ? $routePattern : "$routePattern" . $route['route']; 162 | $this->routes[] = [ 163 | 'method' => $route['method'], 164 | 'route' => $path, 165 | 'handler' => array_merge($router->middlewares, $route['handler']), 166 | ]; 167 | } 168 | } 169 | 170 | /** 171 | * Creates an object to define routes with a specific prefix. 172 | * 173 | * @param string $path The route prefix. 174 | * @return object An anonymous object to define routes with the provided prefix. 175 | */ 176 | public function route(string $path): object 177 | { 178 | $router = $this; 179 | 180 | $methods = self::METHODS; 181 | 182 | return new class($path, $router, $methods) 183 | { 184 | private string $path; 185 | private Router $router; 186 | private array $methods; 187 | 188 | /** 189 | * Constructor for the anonymous class. 190 | * 191 | * @param string $path 192 | * @param Router $router 193 | * @param array $methods 194 | */ 195 | public function __construct(string $path, Router $router, array $methods) 196 | { 197 | $this->path = $path; 198 | $this->router = $router; 199 | $this->methods = $methods; 200 | } 201 | 202 | /** 203 | * Handles dynamic method calls. 204 | * 205 | * @param string $method 206 | * @param array $args 207 | * @return self 208 | * @throws BadMethodCallException 209 | */ 210 | public function __call(string $method, array $args): self 211 | { 212 | // Check if the method is a valid HTTP method 213 | if (in_array(strtoupper($method), $this->methods)) { 214 | $this->router->$method($this->path, ...$args); 215 | } else { 216 | // Throw an exception if the method does not exist 217 | throw new BadMethodCallException("Method $method does not exist"); 218 | } 219 | 220 | return $this; 221 | } 222 | }; 223 | } 224 | 225 | /** 226 | * Adds a route to handle all HTTP methods. 227 | * 228 | * @param string $path The route path. 229 | * @param callable ...$handler The handlers (callbacks) for the route. 230 | */ 231 | public function any(string $path, callable|array ...$handler): void 232 | { 233 | $methods = self::METHODS; 234 | foreach ($methods as $method) { 235 | $this->addRoute($method, $path, $handler); 236 | } 237 | } 238 | 239 | /** 240 | * Adds a route to handle multiple specified HTTP methods. 241 | * 242 | * @param array $methods HTTP methods that the route should handle. 243 | * @param string $path The route path. 244 | * @param callable ...$handler The handlers (callbacks) for the route. 245 | */ 246 | public function match(array $methods, string $path, callable|array ...$handler): void 247 | { 248 | foreach ($methods as $method) { 249 | $this->addRoute($method, $path, $handler); 250 | } 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /src/Orbis/Http/Router.php: -------------------------------------------------------------------------------- 1 | get($path, ...$handler); // Add the GET route to the router 31 | } 32 | 33 | /** 34 | * Adds a POST route to the routes array. 35 | * 36 | * @param string $path The route path. 37 | * @param callable|array $handler The handlers for the route. 38 | */ 39 | function post(string $path, callable|array ...$handler): void 40 | { 41 | // Get the file where the function was called 42 | $caller = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0]; 43 | $callingFile = $caller['file']; 44 | 45 | $key = strtolower($callingFile); 46 | 47 | $router = Orbis::instance($key); 48 | 49 | if (!$router instanceof Router) { 50 | throw new Exception("Invalid router instance: Router not found"); 51 | } 52 | 53 | $router->post($path, ...$handler); // Add the POST route to the router 54 | } 55 | 56 | /** 57 | * Adds a PUT route to the routes array. 58 | * 59 | * @param string $path The route path. 60 | * @param callable|array $handler The handlers for the route. 61 | */ 62 | function put(string $path, callable|array ...$handler): void 63 | { 64 | // Get the file where the function was called 65 | $caller = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0]; 66 | $callingFile = $caller['file']; 67 | 68 | $key = strtolower($callingFile); 69 | 70 | $router = Orbis::instance($key); 71 | 72 | if (!$router instanceof Router) { 73 | throw new Exception("Invalid router instance: Router not found"); 74 | } 75 | 76 | $router->put($path, ...$handler); // Add the PUT route to the router 77 | } 78 | 79 | /** 80 | * Adds a DELETE route to the routes array. 81 | * 82 | * @param string $path The route path. 83 | * @param callable|array $handler The handlers for the route. 84 | */ 85 | function delete(string $path, callable|array ...$handler): void 86 | { 87 | // Get the file where the function was called 88 | $caller = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0]; 89 | $callingFile = $caller['file']; 90 | 91 | $key = strtolower($callingFile); 92 | 93 | $router = Orbis::instance($key); 94 | 95 | if (!$router instanceof Router) { 96 | throw new Exception("Invalid router instance: Router not found"); 97 | } 98 | 99 | $router->delete($path, ...$handler); // Add the DELETE route to the router 100 | } 101 | 102 | /** 103 | * Adds a PATCH route to the routes array. 104 | * 105 | * @param string $path The route path. 106 | * @param callable|array $handler The handlers for the route. 107 | */ 108 | function patch(string $path, callable|array ...$handler): void 109 | { 110 | // Get the file where the function was called 111 | $caller = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0]; 112 | $callingFile = $caller['file']; 113 | 114 | $key = strtolower($callingFile); 115 | 116 | $router = Orbis::instance($key); 117 | 118 | if (!$router instanceof Router) { 119 | throw new Exception("Invalid router instance: Router not found"); 120 | } 121 | 122 | $router->patch($path, ...$handler); // Add the PATCH route to the router 123 | } 124 | 125 | /** 126 | * Adds an OPTIONS route to the routes array. 127 | * 128 | * @param string $path The route path. 129 | * @param callable|array $handler The handlers for the route. 130 | */ 131 | function options(string $path, callable|array ...$handler): void 132 | { 133 | // Get the file where the function was called 134 | $caller = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0]; 135 | $callingFile = $caller['file']; 136 | 137 | $key = strtolower($callingFile); 138 | 139 | $router = Orbis::instance($key); 140 | 141 | if (!$router instanceof Router) { 142 | throw new Exception("Invalid router instance: Router not found"); 143 | } 144 | 145 | $router->options($path, ...$handler); // Add the OPTIONS route to the router 146 | } 147 | 148 | /** 149 | * Adds a HEAD route to the routes array. 150 | * 151 | * @param string $path The route path. 152 | * @param callable|array $handler The handlers for the route. 153 | */ 154 | function head(string $path, callable|array ...$handler): void 155 | { 156 | // Get the file where the function was called 157 | $caller = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0]; 158 | $callingFile = $caller['file']; 159 | 160 | $key = strtolower($callingFile); 161 | 162 | $router = Orbis::instance($key); 163 | 164 | if (!$router instanceof Router) { 165 | throw new Exception("Invalid router instance: Router not found"); 166 | } 167 | 168 | $router->head($path, ...$handler); // Add the HEAD route to the router 169 | } 170 | 171 | /** 172 | * Adds a route to handle all HTTP methods. 173 | * 174 | * @param string $path The route path. 175 | * @param callable ...$handler The handlers (callbacks) for the route. 176 | */ 177 | function any(string $path, callable|array ...$handler): void 178 | { 179 | // Get the file where the function was called 180 | $caller = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0]; 181 | $callingFile = $caller['file']; 182 | 183 | $key = strtolower($callingFile); 184 | 185 | $router = Orbis::instance($key); 186 | 187 | if (!$router instanceof Router) { 188 | throw new Exception("Router not found"); 189 | } 190 | 191 | $router->any($path, ...$handler); // Add the HEAD route to the router 192 | } 193 | 194 | /** 195 | * Configures the router by including a file and returns the Router instance. 196 | * 197 | * @param string $path The path to the router configuration file. 198 | * @return \Lithe\Http\Router The configured Router instance. 199 | * @throws \Exception If the router configuration file cannot be included or if the Router class is not instantiated. 200 | */ 201 | function router(string $path): \Lithe\Http\Router 202 | { 203 | // Replace '/' with the correct directory separator and add '.php' at the end 204 | $normalizedPath = str_replace('/', DIRECTORY_SEPARATOR, $path) . '.php'; 205 | 206 | // Convert the path to lowercase for the registration key 207 | $key = strtolower($normalizedPath); 208 | 209 | // Check if the file exists 210 | if (!file_exists($normalizedPath)) { 211 | throw new \Exception("Router configuration file not found: {$normalizedPath}"); 212 | } 213 | 214 | // Register the Router instance in Orbis 215 | Orbis::register(\Lithe\Http\Router::class, $key); 216 | 217 | // Include the router configuration file 218 | include_once $normalizedPath; 219 | 220 | return Orbis::instance($key, true); // Return the Router instance 221 | } 222 | 223 | /** 224 | * Adds middleware or a router to the application. 225 | * 226 | * @param string|callable|Router|array ...$middleware The middlewares or routers to be added. 227 | */ 228 | function apply(string|callable|Router|array ...$middleware): void 229 | { 230 | // Get the file where the function was called 231 | $caller = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0]; 232 | $callingFile = $caller['file']; 233 | 234 | $key = strtolower($callingFile); 235 | 236 | $router = Orbis::instance($key); 237 | 238 | if (!$router instanceof Router) { 239 | throw new Exception("Invalid router instance: Router not found"); 240 | } 241 | 242 | $router->use(...$middleware); 243 | } 244 | 245 | /** 246 | * Creates an object to define routes with a specific prefix. 247 | * 248 | * @param string $path The route prefix. 249 | * @return object An anonymous object to define routes with the provided prefix. 250 | */ 251 | function route(string $path): object 252 | { 253 | // Get the file where the function was called 254 | $caller = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0]; 255 | $callingFile = $caller['file']; 256 | 257 | $key = strtolower($callingFile); 258 | 259 | $router = Orbis::instance($key); 260 | 261 | if (!$router instanceof Router) { 262 | throw new Exception("Invalid router instance: Router not found"); 263 | } 264 | 265 | return $router->route($path); 266 | } -------------------------------------------------------------------------------- /src/Orbis/helpers.php: -------------------------------------------------------------------------------- 1 | view($file, $data); 33 | } 34 | -------------------------------------------------------------------------------- /tests/AppTest.php: -------------------------------------------------------------------------------- 1 | app = new \Lithe\App; 17 | } 18 | 19 | /** 20 | * Tests the matchRoute method with a valid URL and an invalid URL. 21 | */ 22 | public function testMatchRoute() 23 | { 24 | // Use reflection to access the private method matchRoute 25 | $reflection = new ReflectionClass($this->app); 26 | $method = $reflection->getMethod('matchRoute'); 27 | $method->setAccessible(true); 28 | 29 | // Define some route patterns and URLs for testing 30 | $routePattern = '/user/:id=int'; 31 | $validUrl = '/user/123'; 32 | $invalidUrl = '/user/abc'; 33 | 34 | // Test with a valid URL 35 | $result = $method->invoke($this->app, $routePattern, $validUrl); 36 | $this->assertTrue($result); 37 | 38 | // Test with an invalid URL 39 | $result = $method->invoke($this->app, $routePattern, $invalidUrl); 40 | $this->assertFalse($result); 41 | } 42 | 43 | /** 44 | * Tests the matchRoute method with optional parameters in the URL. 45 | */ 46 | public function testMatchRouteWithOptionalParameters() 47 | { 48 | // Use reflection to access the private method matchRoute 49 | $reflection = new ReflectionClass($this->app); 50 | $method = $reflection->getMethod('matchRoute'); 51 | $method->setAccessible(true); 52 | 53 | // Define some route patterns and URLs for testing 54 | $routePattern = '/user/:id'; 55 | $validUrlWithParam = '/user/123'; 56 | $invalidUrl = '/user/123/extra'; 57 | 58 | // Test with a valid URL containing the parameter 59 | $result = $method->invoke($this->app, $routePattern, $validUrlWithParam); 60 | $this->assertTrue($result); 61 | 62 | // Test with an invalid URL 63 | $result = $method->invoke($this->app, $routePattern, $invalidUrl); 64 | $this->assertFalse($result); 65 | } 66 | 67 | /** 68 | * Tests the matchRoute method with multiple parameters in the URL. 69 | */ 70 | public function testMatchRouteWithMultipleParameters() 71 | { 72 | // Use reflection to access the private method matchRoute 73 | $reflection = new ReflectionClass($this->app); 74 | $method = $reflection->getMethod('matchRoute'); 75 | $method->setAccessible(true); 76 | 77 | // Define some route patterns and URLs for testing 78 | $routePattern = '/user/:id=int/post/:postId=int'; 79 | $validUrl = '/user/123/post/456'; 80 | $invalidUrl = '/user/123/post/abc'; 81 | 82 | // Test with a valid URL 83 | $result = $method->invoke($this->app, $routePattern, $validUrl); 84 | $this->assertTrue($result); 85 | 86 | // Test with an invalid URL 87 | $result = $method->invoke($this->app, $routePattern, $invalidUrl); 88 | $this->assertFalse($result); 89 | } 90 | 91 | /** 92 | * Tests the matchRoute method with complex route patterns. 93 | */ 94 | public function testMatchRouteWithComplexPatterns() 95 | { 96 | // Use reflection to access the private method matchRoute 97 | $reflection = new ReflectionClass($this->app); 98 | $method = $reflection->getMethod('matchRoute'); 99 | $method->setAccessible(true); 100 | 101 | // Define some route patterns and URLs for testing 102 | $routePattern = '/user/:id=int/profile/:section'; 103 | $validUrl = '/user/123/profile/settings'; 104 | $invalidUrl = '/user/123/profile'; 105 | 106 | // Test with a valid URL 107 | $result = $method->invoke($this->app, $routePattern, $validUrl); 108 | $this->assertTrue($result); 109 | 110 | // Test with an invalid URL 111 | $result = $method->invoke($this->app, $routePattern, $invalidUrl); 112 | $this->assertFalse($result); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /tests/Database/.env: -------------------------------------------------------------------------------- 1 | DB_CONNECTION_METHOD=pdo 2 | DB_CONNECTION=mysql 3 | DB_HOST=localhost 4 | DB_NAME=lithe 5 | DB_USERNAME=root 6 | DB_PASSWORD= 7 | DB_SHOULD_INITIATE=true -------------------------------------------------------------------------------- /tests/Database/.gitignore: -------------------------------------------------------------------------------- 1 | /logs -------------------------------------------------------------------------------- /tests/Database/ManagerTest.php: -------------------------------------------------------------------------------- 1 | assertNotNull($result, 'Expected a connection instance, but got null.'); 36 | } 37 | 38 | public function testInitializeWithInvalidEnv() 39 | { 40 | Env::set('DB_SHOULD_INITIATE', false); 41 | $result = Manager::initialize(null, true); 42 | $this->assertNotNull($result, 'Expected a connection instance, but got null when initiating.'); 43 | } 44 | 45 | 46 | public function testConnectionReturnsCurrentConnection() 47 | { 48 | Manager::initialize(); // Inicializa para garantir que temos uma conexão 49 | $connection = Manager::connection(); 50 | $this->assertNotNull($connection, 'Expected to get a connection, but got null.'); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/Http/RequestTest.php: -------------------------------------------------------------------------------- 1 | 'test.txt', 33 | 'type' => 'text/plain', 34 | 'tmp_name' => '/tmp/phpYzdqkD', 35 | 'error' => 0, 36 | 'size' => 123 37 | ]; 38 | 39 | $_FILES['files'] = [ 40 | 'name' => ['file1.txt', 'file2.txt', 'file3.txt'], 41 | 'type' => ['text/plain', 'text/plain', 'text/plain'], 42 | 'tmp_name' => ['/tmp/phpYzdqkD1', '/tmp/phpYzdqkD2', '/tmp/phpYzdqkD3'], 43 | 'error' => [0, 0, 0], 44 | 'size' => [12345, 67890, 23456] 45 | ]; 46 | 47 | $parameters = []; 48 | // Assuming Request.php returns an instance of Request 49 | $this->request = include __DIR__ . '/../../src/Http/Request.php'; 50 | } 51 | 52 | public function testGetMethod() 53 | { 54 | $this->assertEquals('POST', $this->request->method()); // Check the HTTP method 55 | } 56 | 57 | public function testGetIp() 58 | { 59 | $this->assertEquals('127.0.0.1', $this->request->ip); // Check the IP address 60 | } 61 | 62 | public function testGetUrl() 63 | { 64 | $this->assertEquals('/produtos/eletronicos/celulares', $this->request->url); // Check the URL without the script name 65 | } 66 | 67 | public function testGetHeaders() 68 | { 69 | $this->assertArrayHasKey('X-Forwarded-For', $this->request->headers); // Check headers 70 | $this->assertEquals('127.0.0.1', $this->request->headers['X-Forwarded-For']); 71 | } 72 | 73 | public function testFilterEmail() 74 | { 75 | $result = $this->request->filter('email', 'email'); // Filter for email 76 | $this->assertEquals('test@example.com', $result); 77 | } 78 | 79 | public function testFilterInt() 80 | { 81 | $result = $this->request->filter('age', 'int'); // Filter for integer 82 | $this->assertEquals(30, $result); 83 | } 84 | 85 | public function testGetBody() 86 | { 87 | $this->assertEquals('Test', $this->request->body->name); // Check body data 88 | } 89 | 90 | public function testBodyExcludesKeys() 91 | { 92 | $result = $this->request->body(null, ['age']); // Exclude 'age' 93 | $expected = (object)[ 94 | 'name' => 'Test', 95 | 'email' => 'test@example.com' 96 | ]; 97 | $this->assertEquals($expected, $result); 98 | } 99 | 100 | public function testBodyIncludesAndExcludesKeys() 101 | { 102 | $result = $this->request->body(['name', 'email'], ['age']); // Include 'name' and 'email', exclude 'age' 103 | $expected = (object)[ 104 | 'name' => 'Test', 105 | 'email' => 'test@example.com' 106 | ]; 107 | $this->assertEquals($expected, $result); 108 | } 109 | 110 | public function testIsAjax() 111 | { 112 | $this->assertTrue($this->request->isAjax()); // Check if request is an Ajax request 113 | } 114 | 115 | public function testGetCookie() 116 | { 117 | $this->assertEquals('cookie_value', $this->request->cookie('test_cookie')); // Check cookie value 118 | $this->assertNull($this->request->cookie('non_existent_cookie')); // Check non-existent cookie 119 | } 120 | 121 | public function testWantsJson() 122 | { 123 | $this->assertTrue($this->request->wantsJson()); // Check if request accepts JSON 124 | } 125 | 126 | public function testSecure() 127 | { 128 | $this->assertTrue($this->request->secure()); // Check if request is secure (HTTPS) 129 | } 130 | 131 | public function testProtocol() 132 | { 133 | $this->assertEquals('https', $this->request->protocol()); // Check request protocol 134 | } 135 | 136 | public function testInput() 137 | { 138 | $this->assertEquals('Test', $this->request->input('name')); // Check POST input data 139 | $this->assertNull($this->request->input('nonexistent')); // Check non-existent input 140 | } 141 | 142 | public function testHas() 143 | { 144 | $this->assertTrue($this->request->has('name')); // Check if input exists 145 | $this->assertFalse($this->request->has('nonexistent')); // Check non-existent input 146 | } 147 | 148 | public function testIsMethod() 149 | { 150 | $this->assertTrue($this->request->isMethod('POST')); // Check request method 151 | $this->assertFalse($this->request->isMethod('GET')); // Check non-matching method 152 | } 153 | 154 | public function testValidate() 155 | { 156 | $rules = ['name' => 'required']; 157 | $validation = $this->request->validate($rules); // Validate request data 158 | 159 | $this->assertTrue($validation->passed()); // Check validation result 160 | $this->assertEmpty($validation->errors()); // Check if there are any errors 161 | } 162 | 163 | public function testFileMethod() 164 | { 165 | $upload = $this->request->file('file'); // Get file from request 166 | 167 | $this->assertInstanceOf(\Lithe\Base\Upload::class, $upload); // Check file instance 168 | $this->assertTrue($upload->isUploaded()); // Check if file is uploaded 169 | $this->assertEquals('text/plain', $upload->getMimeType()); // Check MIME type 170 | $this->assertEquals(123, $upload->getSize()); // Check file size 171 | } 172 | 173 | public function testFileMethodWithMultipleFiles() 174 | { 175 | $files = $this->request->file('files'); 176 | 177 | // Verifica se a função retorna um array de arquivos 178 | $this->assertIsArray($files); 179 | $this->assertCount(3, $files); 180 | 181 | // Verifica cada arquivo individualmente 182 | $file1 = $files[0]; 183 | $this->assertInstanceOf(Upload::class, $file1); 184 | $this->assertTrue($file1->isUploaded()); 185 | $this->assertEquals('text/plain', $file1->getMimeType()); 186 | $this->assertEquals(12345, $file1->getSize()); 187 | 188 | $file2 = $files[1]; 189 | $this->assertInstanceOf(Upload::class, $file2); 190 | $this->assertTrue($file2->isUploaded()); 191 | $this->assertEquals('text/plain', $file2->getMimeType()); 192 | $this->assertEquals(67890, $file2->getSize()); 193 | 194 | $file3 = $files[2]; 195 | $this->assertInstanceOf(Upload::class, $file3); 196 | $this->assertTrue($file3->isUploaded()); 197 | $this->assertEquals('text/plain', $file3->getMimeType()); 198 | $this->assertEquals(23456, $file3->getSize()); 199 | } 200 | 201 | public function testIs() 202 | { 203 | $this->assertTrue($this->request->is('/produtos/eletronicos/celulares'), 'A URL deveria corresponder exatamente.'); 204 | $this->assertTrue($this->request->is('/produtos/*'), 'A URL deveria corresponder ao padrão /produtos/*.'); 205 | $this->assertTrue($this->request->is('/produtos/eletronicos/*'), 'A URL deveria corresponder ao padrão /produtos/eletronicos/*.'); 206 | $this->assertFalse($this->request->is('/servicos/*'), 'A URL não deveria corresponder ao padrão /servicos/*.'); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /tests/Http/RouterTest.php: -------------------------------------------------------------------------------- 1 | router = new Router; 14 | } 15 | 16 | public function testGetRouteIsAdded() 17 | { 18 | // Define a GET route with the path '/test' 19 | $this->router->get('/test', function () { 20 | return 'Test GET'; 21 | }); 22 | 23 | // Retrieve the registered routes 24 | $routes = $this->getRoutes(); 25 | 26 | // Assert that exactly one route is registered 27 | $this->assertCount(1, $routes); 28 | // Assert that the HTTP method of the route is GET 29 | $this->assertEquals('GET', $routes[0]['method']); 30 | // Assert that the route path is '/test' 31 | $this->assertEquals('/test', $routes[0]['route']); 32 | } 33 | 34 | public function testPostRouteIsAdded() 35 | { 36 | // Define a POST route with the path '/test' 37 | $this->router->post('/test', function () { 38 | return 'Test POST'; 39 | }); 40 | 41 | // Retrieve the registered routes 42 | $routes = $this->getRoutes(); 43 | 44 | // Assert that exactly one route is registered 45 | $this->assertCount(1, $routes); 46 | // Assert that the HTTP method of the route is POST 47 | $this->assertEquals('POST', $routes[0]['method']); 48 | // Assert that the route path is '/test' 49 | $this->assertEquals('/test', $routes[0]['route']); 50 | } 51 | 52 | public function testRouteWithPrefix() 53 | { 54 | // Define a route with a prefix '/api' and a GET method 55 | $this->router->route('/api')->get(function () { 56 | return 'User'; 57 | }); 58 | 59 | // Retrieve the registered routes 60 | $routes = $this->getRoutes(); 61 | 62 | // Assert that exactly one route is registered 63 | $this->assertCount(1, $routes); 64 | // Assert that the HTTP method of the route is GET 65 | $this->assertEquals('GET', $routes[0]['method']); 66 | // Assert that the route path is '/api' 67 | $this->assertEquals('/api', $routes[0]['route']); 68 | } 69 | 70 | public function testAnyMethodRoute() 71 | { 72 | $router = new Router; 73 | 74 | // Define a route that accepts any HTTP method 75 | $router->any('/test', function () { 76 | return 'Any Method'; 77 | }); 78 | 79 | // Retrieve the registered routes 80 | $routes = $this->getRoutes($router); 81 | 82 | // Define the expected routes for different HTTP methods 83 | $expectedRoutes = [ 84 | ['method' => 'GET', 'route' => '/test'], 85 | ['method' => 'POST', 'route' => '/test'], 86 | ['method' => 'PUT', 'route' => '/test'], 87 | ['method' => 'DELETE', 'route' => '/test'], 88 | ['method' => 'PATCH', 'route' => '/test'], 89 | ['method' => 'OPTIONS', 'route' => '/test'], 90 | ['method' => 'HEAD', 'route' => '/test'] 91 | ]; 92 | 93 | // Check if each expected route exists in the registered routes 94 | foreach ($expectedRoutes as $expectedRoute) { 95 | $this->assertTrue( 96 | $this->routeExists($expectedRoute, $routes), 97 | "Failed asserting that the route array contains: " . print_r($expectedRoute, true) 98 | ); 99 | } 100 | } 101 | 102 | /** 103 | * Helper function to check if a route exists in the routes array. 104 | * 105 | * @param array $expectedRoute The expected route. 106 | * @param array $routes The routes to check against. 107 | * @return bool True if the route exists, false otherwise. 108 | */ 109 | private function routeExists(array $expectedRoute, array $routes): bool 110 | { 111 | foreach ($routes as $route) { 112 | if ($route['method'] === $expectedRoute['method'] && $route['route'] === $expectedRoute['route']) { 113 | return true; 114 | } 115 | } 116 | return false; 117 | } 118 | 119 | public function testMatchMethodRoute() 120 | { 121 | $router = new Router; 122 | 123 | // Define a route that matches GET and POST methods 124 | $router->match(['GET', 'POST'], '/test', function () { 125 | return 'Match Method'; 126 | }); 127 | 128 | // Retrieve the registered routes 129 | $routes = $this->getRoutes($router); 130 | 131 | // Assert that there are exactly 2 routes 132 | $this->assertCount(2, $routes); 133 | 134 | // Check if each expected method is present in the routes 135 | foreach (['GET', 'POST'] as $method) { 136 | $this->assertTrue( 137 | $this->routeExists(['method' => $method, 'route' => '/test'], $routes), 138 | "Failed asserting that the route array contains method $method and route /test." 139 | ); 140 | } 141 | } 142 | 143 | private function getRoutes(?Router $router = null): array 144 | { 145 | // Retrieve the 'routes' property from the Router instance using reflection 146 | $router = $router ?? $this->router; 147 | $reflector = new ReflectionClass($router); 148 | $property = $reflector->getProperty('routes'); 149 | $property->setAccessible(true); 150 | return $property->getValue($router); 151 | } 152 | 153 | private function getMiddlewares(?Router $router = null): array 154 | { 155 | // Retrieve the 'middlewares' property from the Router instance using reflection 156 | $router = $router ?? $this->router; 157 | $reflector = new ReflectionClass($router); 158 | $property = $reflector->getProperty('middlewares'); 159 | $property->setAccessible(true); 160 | return $property->getValue($router); 161 | } 162 | } 163 | --------------------------------------------------------------------------------