├── .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 |
5 |
6 |
7 |
8 |
9 |
10 |
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 |
--------------------------------------------------------------------------------