├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Readme.md ├── assets └── LaraUtilX.svg ├── composer.json ├── config ├── feature-toggles.php └── lara-util-x.php ├── database └── migrations │ ├── 2024_01_18_180329_create_access_logs_table.php │ └── 2025_03_06_000254_create_model_audits_table.php ├── phpunit.xml ├── src ├── Enums │ └── LogLevel.php ├── Http │ ├── Controllers │ │ └── CrudController.php │ └── Middleware │ │ └── AccessLogMiddleware.php ├── LLMProviders │ ├── Claude │ │ ├── ClaudeProvider.php │ │ └── Responses │ │ │ └── ClaudeResponse.php │ ├── Contracts │ │ └── LLMProviderInterface.php │ ├── Gemini │ │ ├── GeminiProvider.php │ │ └── Responses │ │ │ └── GeminiResponse.php │ └── OpenAI │ │ ├── OpenAIProvider.php │ │ └── Responses │ │ └── OpenAIResponse.php ├── LaraUtilXServiceProvider.php ├── Models │ └── AccessLog.php ├── Rules │ └── RejectCommonPasswords.php ├── Traits │ ├── ApiResponseTrait.php │ ├── Auditable.php │ └── FileProcessingTrait.php └── Utilities │ ├── CachingUtil.php │ ├── ConfigUtil.php │ ├── FeatureToggleUtil.php │ ├── FilteringUtil.php │ ├── LoggingUtil.php │ ├── PaginationUtil.php │ ├── QueryParameterUtil.php │ ├── RateLimiterUtil.php │ └── SchedulerUtil.php └── tests ├── Feature ├── Traits │ └── ApiResponseTraitFeatureTest.php └── Utilities │ └── CachingUtilFeatureTest.php ├── README.md ├── TestCase.php └── Unit ├── Enums └── LogLevelTest.php ├── Rules └── RejectCommonPasswordsTest.php ├── Traits ├── ApiResponseTraitTest.php └── FileProcessingTraitTest.php └── Utilities ├── CachingUtilTest.php ├── ConfigUtilTest.php ├── FeatureToggleUtilTest.php ├── FilteringUtilTest.php ├── LoggingUtilTest.php ├── PaginationUtilTest.php ├── QueryParameterUtilTest.php ├── RateLimiterUtilTest.php └── SchedulerUtilTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | /.phpunit.cache 2 | /node_modules 3 | /public/build 4 | /public/hot 5 | /public/storage 6 | /storage/*.key 7 | /vendor 8 | .env 9 | .env.backup 10 | .env.production 11 | .phpunit.result.cache 12 | Homestead.json 13 | Homestead.yaml 14 | auth.json 15 | npm-debug.log 16 | yarn-error.log 17 | /.fleet 18 | /.idea 19 | /.vscode 20 | composer.lock -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct - LaraUtilX 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behaviour that contributes to a positive environment for our 15 | community include: 16 | 17 | * Demonstrating empathy and kindness toward other people 18 | * Being respectful of differing opinions, viewpoints, and experiences 19 | * Giving and gracefully accepting constructive feedback 20 | * Accepting responsibility and apologising to those affected by our mistakes, 21 | and learning from the experience 22 | * Focusing on what is best not just for us as individuals, but for the 23 | overall community 24 | 25 | Examples of unacceptable behaviour include: 26 | 27 | * The use of sexualised language or imagery, and sexual attention or advances 28 | * Trolling, insulting or derogatory comments, and personal or political attacks 29 | * Public or private harassment 30 | * Publishing others' private information, such as a physical or email 31 | address, without their explicit permission 32 | * Other conduct which could reasonably be considered inappropriate in a 33 | professional setting 34 | 35 | ## Our Responsibilities 36 | 37 | Project maintainers are responsible for clarifying and enforcing our standards of 38 | acceptable behaviour and will take appropriate and fair corrective action in 39 | response to any instances of unacceptable behaviour. 40 | 41 | Project maintainers have the right and responsibility to remove, edit, or reject 42 | comments, commits, code, wiki edits, issues, and other contributions that are 43 | not aligned to this Code of Conduct, or to ban 44 | temporarily or permanently any contributor for other behaviours that they deem 45 | inappropriate, threatening, offensive, or harmful. 46 | 47 | ## Scope 48 | 49 | This Code of Conduct applies within all community spaces, and also applies when 50 | an individual is officially representing the community in public spaces. 51 | Examples of representing our community include using an official e-mail address, 52 | posting via an official social media account, or acting as an appointed 53 | representative at an online or offline event. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behaviour may be 58 | reported to the community leaders responsible for enforcement at . 59 | All complaints will be reviewed and investigated promptly and fairly. 60 | 61 | All community leaders are obligated to respect the privacy and security of the 62 | reporter of any incident. 63 | 64 | ## Attribution 65 | 66 | This Code of Conduct is adapted from the [Contributor Covenant](https://contributor-covenant.org/), version 67 | [1.4](https://www.contributor-covenant.org/version/1/4/code-of-conduct/code_of_conduct.md) and 68 | [2.0](https://www.contributor-covenant.org/version/2/0/code_of_conduct/code_of_conduct.md), 69 | and was generated by [contributing.md](https://contributing.md/generator). 70 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributing to LaraUtilX 3 | 4 | First off, thanks for taking the time to contribute! ❤️ 5 | 6 | All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉 7 | 8 | > And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about: 9 | > - Star the project 10 | > - Tweet about it 11 | > - Refer this project in your project's readme 12 | > - Mention the project at local meetups and tell your friends/colleagues 13 | 14 | 15 | ## Table of Contents 16 | 17 | - [Code of Conduct](#code-of-conduct) 18 | - [I Have a Question](#i-have-a-question) 19 | - [I Want To Contribute](#i-want-to-contribute) 20 | - [Reporting Bugs](#reporting-bugs) 21 | - [Suggesting Enhancements](#suggesting-enhancements) 22 | - [Your First Code Contribution](#your-first-code-contribution) 23 | - [Improving The Documentation](#improving-the-documentation) 24 | - [Styleguides](#styleguides) 25 | - [Commit Messages](#commit-messages) 26 | - [Join The Project Team](#join-the-project-team) 27 | 28 | 29 | ## Code of Conduct 30 | 31 | This project and everyone participating in it is governed by the 32 | [LaraUtilX Code of Conduct](https://github.com/omarchouman/lara-util-x/blob/master/CODE_OF_CONDUCT.md). 33 | By participating, you are expected to uphold this code. Please report unacceptable behavior 34 | to . 35 | 36 | 37 | ## I Have a Question 38 | 39 | > If you want to ask a question, we assume that you have read the available [Documentation](https://larautilx.omarchouman.com/). 40 | 41 | Before you ask a question, it is best to search for existing [Issues](https://github.com/omarchouman/lara-util-x/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. It is also advisable to search the internet for answers first. 42 | 43 | If you then still feel the need to ask a question and need clarification, we recommend the following: 44 | 45 | - Open an [Issue](https://github.com/omarchouman/lara-util-x/issues/new). 46 | - Provide as much context as you can about what you're running into. 47 | - Provide project and platform versions (nodejs, npm, etc), depending on what seems relevant. 48 | 49 | We will then take care of the issue as soon as possible. 50 | 51 | 65 | 66 | ## I Want To Contribute 67 | 68 | > ### Legal Notice 69 | > When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project licence. 70 | 71 | ### Reporting Bugs 72 | 73 | 74 | #### Before Submitting a Bug Report 75 | 76 | A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible. 77 | 78 | - Make sure that you are using the latest version. 79 | - Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (Make sure that you have read the [documentation](https://larautilx.omarchouman.com/). If you are looking for support, you might want to check [this section](#i-have-a-question)). 80 | - To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker](https://github.com/omarchouman/lara-util-x/issues?q=label%3Abug). 81 | - Also make sure to search the internet (including Stack Overflow) to see if users outside of the GitHub community have discussed the issue. 82 | - Collect information about the bug: 83 | - Stack trace (Traceback) 84 | - OS, Platform and Version (Windows, Linux, macOS, x86, ARM) 85 | - Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant. 86 | - Possibly your input and the output 87 | - Can you reliably reproduce the issue? And can you also reproduce it with older versions? 88 | 89 | 90 | #### How Do I Submit a Good Bug Report? 91 | 92 | > You must never report security related issues, vulnerabilities or bugs including sensitive information to the issue tracker, or elsewhere in public. Instead sensitive bugs must be sent by email to . 93 | 94 | 95 | We use GitHub issues to track bugs and errors. If you run into an issue with the project: 96 | 97 | - Open an [Issue](https://github.com/omarchouman/lara-util-x/issues/new). (Since we can't be sure at this point whether it is a bug or not, we ask you not to talk about a bug yet and not to label the issue.) 98 | - Explain the behavior you would expect and the actual behavior. 99 | - Please provide as much context as possible and describe the *reproduction steps* that someone else can follow to recreate the issue on their own. This usually includes your code. For good bug reports you should isolate the problem and create a reduced test case. 100 | - Provide the information you collected in the previous section. 101 | 102 | Once it's filed: 103 | 104 | - The project team will label the issue accordingly. 105 | - A team member will try to reproduce the issue with your provided steps. If there are no reproduction steps or no obvious way to reproduce the issue, the team will ask you for those steps and mark the issue as `needs-repro`. Bugs with the `needs-repro` tag will not be addressed until they are reproduced. 106 | - If the team is able to reproduce the issue, it will be marked `needs-fix`, as well as possibly other tags (such as `critical`), and the issue will be left to be [implemented by someone](#your-first-code-contribution). 107 | 108 | 109 | 110 | 111 | ### Suggesting Enhancements 112 | 113 | This section guides you through submitting an enhancement suggestion for LaraUtilX, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community to understand your suggestion and find related suggestions. 114 | 115 | 116 | #### Before Submitting an Enhancement 117 | 118 | - Make sure that you are using the latest version. 119 | - Read the [documentation](https://larautilx.omarchouman.com/) carefully and find out if the functionality is already covered, maybe by an individual configuration. 120 | - Perform a [search](https://github.com/omarchouman/lara-util-x/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one. 121 | - Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library. 122 | 123 | 124 | #### How Do I Submit a Good Enhancement Suggestion? 125 | 126 | Enhancement suggestions are tracked as [GitHub issues](https://github.com/omarchouman/lara-util-x/issues). 127 | 128 | - Use a **clear and descriptive title** for the issue to identify the suggestion. 129 | - Provide a **step-by-step description of the suggested enhancement** in as many details as possible. 130 | - **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point you can also tell which alternatives do not work for you. 131 | - You may want to **include screenshots or screen recordings** which help you demonstrate the steps or point out the part which the suggestion is related to. You can use [LICEcap](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and the built-in [screen recorder in GNOME](https://help.gnome.org/users/gnome-help/stable/screen-shot-record.html.en) or [SimpleScreenRecorder](https://github.com/MaartenBaert/ssr) on Linux. 132 | - **Explain why this enhancement would be useful** to most LaraUtilX users. You may also want to point out the other projects that solved it better and which could serve as inspiration. 133 | 134 | 135 | 136 | ### Your First Code Contribution 137 | 141 | 142 | ### Improving The Documentation 143 | 147 | 148 | ## Styleguides 149 | ### Commit Messages 150 | 153 | 154 | ## Join The Project Team 155 | 156 | 157 | 158 | ## Attribution 159 | This guide is based on the [contributing.md](https://contributing.md/generator)! 160 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Omar Chouman 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 | # LaraUtilX: Unleash the Power of Laravel with Utilities and Helpers 2 | 3 |
4 | LaraUtilX Logo 5 |
6 | 7 | LaraUtilX is a comprehensive Laravel package designed to supercharge your development experience by providing a suite of utility classes, helpful traits, middleware, and more. Whether you're a seasoned Laravel developer or just getting started, LaraUtilX offers a collection of tools to streamline common tasks and enhance the functionality of your Laravel applications. 8 | 9 | **Version:** 1.3.0 10 | **Laravel Support:** Laravel 8.0+ 11 | **PHP Support:** PHP 8.0+ 12 | **License:** MIT 13 | 14 | --- 15 | 16 | 📘 **Full Documentation** 17 | Explore full usage examples, configuration options, and best practices at: 18 | 👉 [https://larautilx.omarchouman.com](https://larautilx.omarchouman.com) 19 | 20 | --- 21 | 22 | ## Key Features 23 | 24 | 1. **CrudController:** Simplify your CRUD operations with the generic `CrudController` that can be easily extended, allowing you to create, read, update, and delete records effortlessly. 25 | 26 | 2. **ApiResponseTrait:** Craft consistent and standardized API responses with the `ApiResponseTrait`. This trait provides helper methods for formatting JSON responses, making your API endpoints clean and well-structured. 27 | 28 | 3. **FileProcessingTrait:** Manage file uploads and deletions seamlessly with the `FileProcessingTrait`. This trait offers methods for uploading single or multiple files, deleting files, and now retrieving file contents. 29 | 30 | 4. **ValidationHelperTrait:** Validate user input with ease using the `ValidationHelperTrait`. This trait includes handy methods for common validation scenarios, such as email addresses, phone numbers, and strong passwords. 31 | 32 | 5. **SchedulerMonitor:** Keep an eye on your scheduled tasks with the `SchedulerUtil` utility. Monitor upcoming scheduled events, check if tasks are overdue, and gain insights into the status of your scheduled jobs. 33 | 34 | 6. **FilteringUtil:** Effortlessly filter data based on specified criteria with the `FilteringUtil`. This utility provides a convenient way to filter collections or arrays based on field names, operators, and values. 35 | 36 | 7. **AccessLogMiddleware:** LaraUtilX includes middleware components like the `AccessLogMiddleware` to log access to your application, adding an extra layer of security and accountability. 37 | 38 | 8. **PaginationUtil:** Seamlessly handle paginated data with LaraUtilX's `PaginationUtil`. This utility simplifies the process of paginating query results, allowing you to customize the number of items per page, navigate through paginated results effortlessly, and present data in a user-friendly manner. 39 | 40 | 9. **CachingUtil:** Optimize performance and reduce database queries using LaraUtilX's `CachingUtil`. Store and retrieve frequently accessed data with ease, taking advantage of features like customizable cache expiration and cache tags. 41 | 42 | 10. **ConfigUtil:** Manage your Laravel configuration settings effortlessly with the `ConfigUtil`. Retrieve, set defaults, and dynamically manipulate configuration data. Simplify the way you interact with your application's configuration, ensuring a clean and organized approach. 43 | 44 | 11. **LLM Providers:** Effortlessly integrate advanced AI-powered chat completions into your Laravel application with our LLM providers. Choose between OpenAI's GPT models or Google's Gemini models through a unified interface. Both providers support all major chat parameters, automatic retry logic, and structured responses. Generate dynamic, context-aware text completions for your users with just a few lines of code. 45 | 46 | - **OpenAIProvider:** Interact with OpenAI's GPT models (GPT-3.5, GPT-4, etc.) 47 | - **GeminiProvider:** Interact with Google's Gemini models (Gemini 2.0 Flash, etc.) 48 | - **Configurable Provider Selection:** Switch between providers via configuration 49 | - **Unified Interface:** Same API for both providers with automatic model selection 50 | 51 | 12. **FeatureToggleUtil:** Implement feature flags and toggles with ease using the `FeatureToggleUtil`. Enable or disable features dynamically based on configuration, user context, or environment settings. Perfect for A/B testing, gradual rollouts, and feature management. 52 | 53 | 13. **LoggingUtil:** Enhance your application's logging capabilities with the `LoggingUtil`. Create structured logs with JSON formatting, custom channels, and contextual information. Includes predefined methods for all log levels with automatic timestamp and environment tracking. 54 | 55 | 14. **QueryParameterUtil:** Parse and validate query parameters from HTTP requests with the `QueryParameterUtil`. Safely extract and filter query parameters based on allowed lists, improving API security and data handling. 56 | 57 | 15. **RateLimiterUtil:** Implement rate limiting for your APIs and endpoints using the `RateLimiterUtil`. Control request frequency, prevent abuse, and manage API usage with configurable limits and decay times. 58 | 59 | 16. **Auditable Trait:** Automatically track model changes with the `Auditable` trait. Log create, update, and delete operations with user context, old values, and new values. Perfect for audit trails and compliance requirements. 60 | 61 | 17. **RejectCommonPasswords Rule:** Strengthen password security with the `RejectCommonPasswords` validation rule. Prevent users from using common, easily guessable passwords with a comprehensive list of weak passwords. 62 | 63 | ## Test Suite 64 | 65 | LaraUtilX comes with a comprehensive test suite that ensures reliability and quality. The test suite includes both unit tests and feature tests covering all utilities, traits, and components. 66 | 67 | ### Running Tests 68 | 69 | #### Prerequisites 70 | 71 | Make sure you have installed the development dependencies: 72 | 73 | ```bash 74 | composer install --dev 75 | ``` 76 | 77 | #### Run All Tests 78 | 79 | ```bash 80 | ./vendor/bin/phpunit 81 | ``` 82 | 83 | #### Run Specific Test Suites 84 | 85 | ```bash 86 | # Run only unit tests 87 | ./vendor/bin/phpunit --testsuite Unit 88 | 89 | # Run only feature tests 90 | ./vendor/bin/phpunit --testsuite Feature 91 | ``` 92 | 93 | #### Run Specific Test Classes 94 | 95 | ```bash 96 | # Run tests for a specific utility 97 | ./vendor/bin/phpunit tests/Unit/Utilities/CachingUtilTest.php 98 | 99 | # Run tests for a specific trait 100 | ./vendor/bin/phpunit tests/Unit/Traits/ApiResponseTraitTest.php 101 | ``` 102 | 103 | #### Run Tests with Coverage 104 | 105 | ```bash 106 | ./vendor/bin/phpunit --coverage-html coverage 107 | ``` 108 | 109 | ### Test Coverage 110 | 111 | The test suite provides comprehensive coverage for: 112 | 113 | - **Utilities**: All utility classes with their methods and edge cases 114 | - **Traits**: All traits with their functionality and integration 115 | - **Enums**: All enum values and behaviors 116 | - **Rules**: Validation rules with various input scenarios 117 | - **Feature Tests**: Integration scenarios and performance tests 118 | 119 | ### Test Structure 120 | 121 | ``` 122 | tests/ 123 | ├── TestCase.php # Base test case with Laravel setup 124 | ├── Unit/ # Unit tests for individual components 125 | │ ├── Enums/ 126 | │ │ └── LogLevelTest.php 127 | │ ├── Rules/ 128 | │ │ └── RejectCommonPasswordsTest.php 129 | │ ├── Traits/ 130 | │ │ ├── ApiResponseTraitTest.php 131 | │ │ └── FileProcessingTraitTest.php 132 | │ └── Utilities/ 133 | │ ├── CachingUtilTest.php 134 | │ ├── ConfigUtilTest.php 135 | │ ├── FeatureToggleUtilTest.php 136 | │ ├── FilteringUtilTest.php 137 | │ ├── LoggingUtilTest.php 138 | │ ├── PaginationUtilTest.php 139 | │ ├── QueryParameterUtilTest.php 140 | │ ├── RateLimiterUtilTest.php 141 | │ └── SchedulerUtilTest.php 142 | └── Feature/ # Integration tests 143 | ├── Traits/ 144 | │ └── ApiResponseTraitFeatureTest.php 145 | └── Utilities/ 146 | └── CachingUtilFeatureTest.php 147 | ``` 148 | 149 | ## How to Get Started 150 | 151 | 1. Install LaraUtilX using Composer: 152 | ```bash 153 | composer require omarchouman/lara-util-x 154 | ``` 155 | 2. Explore the included utilities, traits, and middleware in your Laravel project. 156 | 3. Customize and extend LaraUtilX to match the specific needs of your application. 157 | 158 | ## Development & Contributing 159 | 160 | LaraUtilX is actively maintained and welcomes contributions! The package includes a comprehensive test suite to ensure reliability and quality. 161 | 162 | ### Development Setup 163 | 164 | 1. Clone the repository 165 | 2. Install dependencies: `composer install --dev` 166 | 3. Run tests: `./vendor/bin/phpunit` 167 | 4. Check code coverage: `./vendor/bin/phpunit --coverage-html coverage` 168 | 169 | ### Writing Tests 170 | 171 | When contributing new features: 172 | - Write unit tests for individual components 173 | - Write feature tests for integration scenarios 174 | - Maintain high test coverage (90%+) 175 | - Follow the AAA pattern: Arrange, Act, Assert 176 | - Test edge cases and error conditions 177 | 178 | ### Code Quality 179 | 180 | - Follow PSR-12 coding standards 181 | - Use descriptive variable and method names 182 | - Include comprehensive docblocks 183 | - Handle errors gracefully 184 | - Write clean, maintainable code 185 | 186 | Save time, enhance code readability, and boost your Laravel projects with LaraUtilX – the toolkit every Laravel developer deserves. -------------------------------------------------------------------------------- /assets/LaraUtilX.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "omarchouman/lara-util-x", 3 | "description": "LaraUtilX is a comprehensive Laravel package that empowers developers with a rich set of utilities, helpful traits, middleware, and more. Streamline common tasks, enhance code readability, and boost the functionality of your Laravel applications with this versatile toolkit.", 4 | "license": "MIT", 5 | "version": "1.3.0", 6 | "authors": [ 7 | { 8 | "name": "omarchouman", 9 | "email": "omar.chouman0@gmail.com" 10 | } 11 | ], 12 | "minimum-stability": "dev", 13 | "require": {}, 14 | "require-dev": { 15 | "phpunit/phpunit": "^10.0", 16 | "orchestra/testbench": "^8.0", 17 | "mockery/mockery": "^1.6.12" 18 | }, 19 | "autoload": { 20 | "psr-4": { 21 | "LaraUtilX\\": "src/" 22 | } 23 | }, 24 | "autoload-dev": { 25 | "psr-4": { 26 | "LaraUtilX\\Tests\\": "tests/" 27 | } 28 | }, 29 | "extra": { 30 | "laravel": { 31 | "providers": [ 32 | "LaraUtilX\\LaraUtilXServiceProvider" 33 | ] 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /config/feature-toggles.php: -------------------------------------------------------------------------------- 1 | false, 20 | ]; -------------------------------------------------------------------------------- /config/lara-util-x.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'default_provider' => env('LLM_DEFAULT_PROVIDER', 'openai'), // openai | gemini | claude 6 | ], 7 | 8 | 'cache' => [ 9 | 'default_expiration' => 60, 10 | 'default_tags' => [], 11 | ], 12 | 13 | 'rate_limiting' => [ 14 | 'default_max_attempts' => 60, 15 | 'default_decay_minutes' => 1, 16 | 'cache_prefix' => 'rate_limit:', 17 | 'defaults' => [ 18 | 'api' => [ 19 | 'max_attempts' => 60, 20 | 'decay_minutes' => 1, 21 | ], 22 | 'auth' => [ 23 | 'max_attempts' => 5, 24 | 'decay_minutes' => 15, 25 | ], 26 | 'download' => [ 27 | 'max_attempts' => 3, 28 | 'decay_minutes' => 1, 29 | ], 30 | ], 31 | ], 32 | 33 | 'openai' => [ 34 | 'api_key' => env('OPENAI_API_KEY'), 35 | 'max_retries' => env('OPENAI_MAX_RETRIES', 3), 36 | 'retry_delay' => env('OPENAI_RETRY_DELAY', 2), 37 | 'default_model' => env('OPENAI_DEFAULT_MODEL', 'gpt-3.5-turbo'), 38 | 'default_temperature' => env('OPENAI_DEFAULT_TEMPERATURE', 0.7), 39 | 'default_max_tokens' => env('OPENAI_DEFAULT_MAX_TOKENS', 300), 40 | 'default_top_p' => env('OPENAI_DEFAULT_TOP_P', 1.0), 41 | ], 42 | 43 | 'gemini' => [ 44 | 'api_key' => env('GEMINI_API_KEY'), 45 | 'max_retries' => env('GEMINI_MAX_RETRIES', 3), 46 | 'retry_delay' => env('GEMINI_RETRY_DELAY', 2), 47 | 'base_url' => env('GEMINI_BASE_URL', 'https://generativelanguage.googleapis.com/v1beta'), 48 | 'default_model' => env('GEMINI_DEFAULT_MODEL', 'gemini-2.0-flash'), 49 | 'default_temperature' => env('GEMINI_DEFAULT_TEMPERATURE', 0.7), 50 | 'default_max_tokens' => env('GEMINI_DEFAULT_MAX_TOKENS', 300), 51 | 'default_top_p' => env('GEMINI_DEFAULT_TOP_P', 1.0), 52 | ], 53 | 54 | 'claude' => [ 55 | 'api_key' => env('CLAUDE_API_KEY'), 56 | 'max_retries' => env('CLAUDE_MAX_RETRIES', 3), 57 | 'retry_delay' => env('CLAUDE_RETRY_DELAY', 2), 58 | 'base_url' => env('CLAUDE_BASE_URL', 'https://api.anthropic.com'), 59 | 'default_model' => env('CLAUDE_DEFAULT_MODEL', 'claude-3-5-sonnet-20241022'), 60 | 'default_temperature' => env('CLAUDE_DEFAULT_TEMPERATURE', 1.0), 61 | 'default_max_tokens' => env('CLAUDE_DEFAULT_MAX_TOKENS', 1024), 62 | 'default_top_p' => env('CLAUDE_DEFAULT_TOP_P', 1.0), 63 | ], 64 | ]; -------------------------------------------------------------------------------- /database/migrations/2024_01_18_180329_create_access_logs_table.php: -------------------------------------------------------------------------------- 1 | id(); 15 | $table->ipAddress('ip'); 16 | $table->string('method'); 17 | $table->text('url'); 18 | $table->text('user_agent')->nullable(); 19 | $table->json('request_data')->nullable(); 20 | $table->timestamps(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | */ 27 | public function down(): void 28 | { 29 | Schema::dropIfExists('access_logs'); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /database/migrations/2025_03_06_000254_create_model_audits_table.php: -------------------------------------------------------------------------------- 1 | id(); 12 | $table->string('model_type'); // Model class name 13 | $table->unsignedBigInteger('model_id')->nullable(); // Model primary key 14 | $table->string('event'); // Created, updated, deleted 15 | $table->json('old_values')->nullable(); 16 | $table->json('new_values')->nullable(); 17 | $table->unsignedBigInteger('user_id')->nullable(); // Authenticated user 18 | $table->timestamps(); 19 | }); 20 | } 21 | 22 | public function down(): void 23 | { 24 | Schema::dropIfExists('model_audits'); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 17 | tests/Unit 18 | 19 | 20 | tests/Feature 21 | 22 | 23 | 24 | 25 | src 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/Enums/LogLevel.php: -------------------------------------------------------------------------------- 1 | model = $model; 23 | } 24 | 25 | public function getAllRecords(Request $request): JsonResponse 26 | { 27 | $query = $this->model->query(); 28 | 29 | if (!empty($this->searchableFields) && $request->has('search')) { 30 | $searchTerm = $request->get('search'); 31 | $query->where(function ($q) use ($searchTerm) { 32 | foreach ($this->searchableFields as $field) { 33 | $q->orWhere($field, 'LIKE', "%{$searchTerm}%"); 34 | } 35 | }); 36 | } 37 | 38 | // Load relationships if defined 39 | if (!empty($this->relationships)) { 40 | $query->with($this->relationships); 41 | } 42 | 43 | if ($request->has('sort_by')) { 44 | $direction = $request->get('sort_direction', 'asc'); 45 | $query->orderBy($request->get('sort_by'), $direction); 46 | } 47 | 48 | $records = $query->paginate($request->get('per_page', $this->perPage)); 49 | 50 | return response()->json([ 51 | 'data' => $records->items(), 52 | 'meta' => [ 53 | 'current_page' => $records->currentPage(), 54 | 'last_page' => $records->lastPage(), 55 | 'per_page' => $records->perPage(), 56 | 'total' => $records->total(), 57 | ] 58 | ]); 59 | } 60 | 61 | 62 | public function getRecordById($id): JsonResponse 63 | { 64 | $query = $this->model->query(); 65 | 66 | if (!empty($this->relationships)) { 67 | $query->with($this->relationships); 68 | } 69 | 70 | $record = $query->findOrFail($id); 71 | 72 | return response()->json(['data' => $record]); 73 | } 74 | 75 | 76 | public function storeRecord(Request $request): JsonResponse 77 | { 78 | $validated = $this->validateRequest($request); 79 | 80 | $record = $this->model->create($validated); 81 | 82 | if (!empty($this->relationships)) { 83 | $record->load($this->relationships); 84 | } 85 | 86 | return response()->json([ 87 | 'message' => 'Record created successfully', 88 | 'data' => $record 89 | ], 201); 90 | } 91 | 92 | 93 | public function updateRecord(Request $request, $id): JsonResponse 94 | { 95 | $record = $this->model->findOrFail($id); 96 | 97 | $validated = $this->validateRequest($request, $id); 98 | 99 | $record->update($validated); 100 | 101 | if (!empty($this->relationships)) { 102 | $record->load($this->relationships); 103 | } 104 | 105 | return response()->json([ 106 | 'message' => 'Record updated successfully', 107 | 'data' => $record 108 | ]); 109 | } 110 | 111 | 112 | public function deleteRecord($id): JsonResponse 113 | { 114 | $record = $this->model->findOrFail($id); 115 | $record->delete(); 116 | 117 | return response()->json([ 118 | 'message' => 'Record deleted successfully' 119 | ], 204); 120 | } 121 | 122 | 123 | protected function validateRequest(Request $request, $id = null): array 124 | { 125 | if (empty($this->validationRules)) { 126 | return $request->all(); 127 | } 128 | 129 | $rules = $this->validationRules; 130 | 131 | if ($id) { 132 | foreach ($rules as $field => $rule) { 133 | if (is_string($rule) && str_contains($rule, 'unique:')) { 134 | $rules[$field] = $rule . ',' . $id; 135 | } 136 | } 137 | } 138 | 139 | return $request->validate($rules); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/Http/Middleware/AccessLogMiddleware.php: -------------------------------------------------------------------------------- 1 | $request->ip() ? $request->ip() : null, 15 | 'method' => $request->method() ? $request->method() : null, 16 | 'url' => $request->fullUrl() ? $request->fullUrl() : null, 17 | 'user_agent' => $request->header('User-Agent') ? $request->header('User-Agent') : null, 18 | 'request_data' => $request->all() ? json_encode($request->all()) : null, 19 | ]; 20 | 21 | AccessLog::create($logData); 22 | 23 | return $next($request); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/LLMProviders/Claude/ClaudeProvider.php: -------------------------------------------------------------------------------- 1 | apiKey = $apiKey; 24 | $this->maxRetries = $maxRetries; 25 | $this->retryDelay = $retryDelay; 26 | $this->baseUrl = rtrim($baseUrl, '/'); 27 | } 28 | 29 | public function generateResponse( 30 | string $modelName, 31 | array $messages, 32 | ?float $temperature = null, 33 | ?int $maxTokens = null, 34 | ?array $stop = null, 35 | ?float $topP = null, 36 | ?float $frequencyPenalty = null, 37 | ?float $presencePenalty = null, 38 | ?array $logitBias = null, 39 | ?string $user = null, 40 | ?bool $jsonMode = false, 41 | bool $fullResponse = false 42 | ): ClaudeResponse { 43 | $endpoint = $this->baseUrl . '/v1/messages'; 44 | 45 | $payload = $this->buildPayload( 46 | modelName: $modelName, 47 | messages: $messages, 48 | temperature: $temperature, 49 | maxTokens: $maxTokens, 50 | stop: $stop, 51 | topP: $topP, 52 | jsonMode: $jsonMode 53 | ); 54 | 55 | return $this->executeWithRetry(function () use ($endpoint, $payload, $fullResponse) { 56 | $response = Http::withHeaders([ 57 | 'Content-Type' => 'application/json', 58 | 'x-api-key' => $this->apiKey, 59 | 'anthropic-version' => '2023-06-01' 60 | ])->post($endpoint, $payload); 61 | 62 | if (!$response->successful()) { 63 | $body = $response->json(); 64 | $message = $body['error']['message'] ?? 'Claude API request failed'; 65 | throw new \RuntimeException($message); 66 | } 67 | 68 | $data = $response->json(); 69 | 70 | $content = ''; 71 | if (isset($data['content'][0]['text'])) { 72 | $content = $data['content'][0]['text']; 73 | } 74 | 75 | return new ClaudeResponse( 76 | content: $content, 77 | model: $data['model'] ?? null, 78 | usage: (object) ($data['usage'] ?? []), 79 | rawResponse: $fullResponse ? (object) $data : null 80 | ); 81 | }); 82 | } 83 | 84 | private function buildPayload( 85 | string $modelName, 86 | array $messages, 87 | ?float $temperature, 88 | ?int $maxTokens, 89 | ?array $stop, 90 | ?float $topP, 91 | ?bool $jsonMode 92 | ): array { 93 | $payload = [ 94 | 'model' => $modelName, 95 | 'messages' => $messages, 96 | 'max_tokens' => $maxTokens ?? 1024, 97 | ]; 98 | 99 | if ($temperature !== null) { 100 | $payload['temperature'] = $temperature; 101 | } 102 | 103 | if ($topP !== null) { 104 | $payload['top_p'] = $topP; 105 | } 106 | 107 | if ($stop !== null && !empty($stop)) { 108 | $payload['stop_sequences'] = $stop; 109 | } 110 | 111 | // Claude doesn't support frequency_penalty and presence_penalty like OpenAI 112 | // These parameters are ignored for Claude 113 | 114 | return $payload; 115 | } 116 | 117 | /** 118 | * @template T 119 | * @param callable(): T $callback 120 | * @return T 121 | */ 122 | private function executeWithRetry(callable $callback) 123 | { 124 | $attempt = 0; 125 | $lastException = null; 126 | 127 | while ($attempt < $this->maxRetries) { 128 | try { 129 | return $callback(); 130 | } catch (\Throwable $e) { 131 | $lastException = $e; 132 | $attempt++; 133 | 134 | if ($attempt < $this->maxRetries) { 135 | Log::warning("Claude API request failed, retrying...", [ 136 | 'attempt' => $attempt, 137 | 'error' => $e->getMessage() 138 | ]); 139 | sleep($this->retryDelay); 140 | } 141 | } 142 | } 143 | 144 | Log::error("Claude API request failed after {$this->maxRetries} attempts", [ 145 | 'error' => $lastException?->getMessage() 146 | ]); 147 | 148 | throw $lastException ?? new \RuntimeException('Unknown Claude error'); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/LLMProviders/Claude/Responses/ClaudeResponse.php: -------------------------------------------------------------------------------- 1 | content; 19 | } 20 | 21 | public function getModel(): ?string 22 | { 23 | return $this->model; 24 | } 25 | 26 | public function getUsage(): ?object 27 | { 28 | return $this->usage; 29 | } 30 | 31 | public function getRawResponse(): ?object 32 | { 33 | return $this->rawResponse; 34 | } 35 | 36 | public function toArray(): array 37 | { 38 | return [ 39 | 'content' => $this->content, 40 | 'model' => $this->model, 41 | 'usage' => $this->usage, 42 | ]; 43 | } 44 | 45 | public function toJson(): string 46 | { 47 | return json_encode($this->toArray()); 48 | } 49 | 50 | public function jsonSerialize(): array 51 | { 52 | return $this->toArray(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/LLMProviders/Contracts/LLMProviderInterface.php: -------------------------------------------------------------------------------- 1 | apiKey = $apiKey; 24 | $this->maxRetries = $maxRetries; 25 | $this->retryDelay = $retryDelay; 26 | $this->baseUrl = rtrim($baseUrl, '/'); 27 | } 28 | 29 | public function generateResponse( 30 | string $modelName, 31 | array $messages, 32 | ?float $temperature = null, 33 | ?int $maxTokens = null, 34 | ?array $stop = null, 35 | ?float $topP = null, 36 | ?float $frequencyPenalty = null, 37 | ?float $presencePenalty = null, 38 | ?array $logitBias = null, 39 | ?string $user = null, 40 | ?bool $jsonMode = false, 41 | bool $fullResponse = false 42 | ): GeminiResponse { 43 | $endpoint = $this->baseUrl . '/models/' . $modelName . ':generateContent?key=' . urlencode($this->apiKey); 44 | 45 | $contents = $this->mapMessagesToGeminiContents($messages, $jsonMode === true); 46 | 47 | $generationConfig = []; 48 | if ($temperature !== null) { 49 | $generationConfig['temperature'] = $temperature; 50 | } 51 | if ($maxTokens !== null) { 52 | $generationConfig['maxOutputTokens'] = $maxTokens; 53 | } 54 | if ($topP !== null) { 55 | $generationConfig['topP'] = $topP; 56 | } 57 | if ($stop !== null) { 58 | $generationConfig['stopSequences'] = $stop; 59 | } 60 | 61 | $payload = [ 62 | 'contents' => $contents, 63 | ]; 64 | if (!empty($generationConfig)) { 65 | $payload['generationConfig'] = $generationConfig; 66 | } 67 | 68 | return $this->executeWithRetry(function () use ($endpoint, $payload, $fullResponse, $jsonMode) { 69 | $response = Http::withHeaders([ 70 | 'Content-Type' => 'application/json' 71 | ])->post($endpoint, $payload); 72 | 73 | if (!$response->successful()) { 74 | $body = $response->json(); 75 | $message = $body['error']['message'] ?? 'Gemini API request failed'; 76 | throw new \RuntimeException($message); 77 | } 78 | 79 | $data = $response->json(); 80 | 81 | $text = ''; 82 | if (isset($data['candidates'][0]['content']['parts'])) { 83 | foreach ($data['candidates'][0]['content']['parts'] as $part) { 84 | if (isset($part['text'])) { 85 | $text .= $part['text']; 86 | } 87 | } 88 | } 89 | 90 | return new GeminiResponse( 91 | content: $text, 92 | model: $data['model'] ?? null, 93 | usage: (object) ($data['usageMetadata'] ?? []), 94 | rawResponse: $fullResponse ? (object) $data : null 95 | ); 96 | }); 97 | } 98 | 99 | private function mapMessagesToGeminiContents(array $messages, bool $jsonMode): array 100 | { 101 | $contents = []; 102 | foreach ($messages as $message) { 103 | $role = $message['role'] ?? 'user'; 104 | $text = $message['content'] ?? ''; 105 | 106 | // Gemini uses 'user' and 'model' roles 107 | if ($role === 'assistant') { 108 | $role = 'model'; 109 | } 110 | 111 | $parts = [ 112 | ['text' => $text] 113 | ]; 114 | 115 | if ($jsonMode === true) { 116 | $parts = [ 117 | [ 118 | 'text' => $text 119 | ] 120 | ]; 121 | } 122 | 123 | $contents[] = [ 124 | 'role' => $role, 125 | 'parts' => $parts 126 | ]; 127 | } 128 | 129 | return $contents; 130 | } 131 | 132 | /** 133 | * @template T 134 | * @param callable(): T $callback 135 | * @return T 136 | */ 137 | private function executeWithRetry(callable $callback) 138 | { 139 | $attempt = 0; 140 | $lastException = null; 141 | 142 | while ($attempt < $this->maxRetries) { 143 | try { 144 | return $callback(); 145 | } catch (\Throwable $e) { 146 | $lastException = $e; 147 | $attempt++; 148 | 149 | if ($attempt < $this->maxRetries) { 150 | Log::warning("Gemini API request failed, retrying...", [ 151 | 'attempt' => $attempt, 152 | 'error' => $e->getMessage() 153 | ]); 154 | sleep($this->retryDelay); 155 | } 156 | } 157 | } 158 | 159 | Log::error("Gemini API request failed after {$this->maxRetries} attempts", [ 160 | 'error' => $lastException?->getMessage() 161 | ]); 162 | 163 | throw $lastException ?? new \RuntimeException('Unknown Gemini error'); 164 | } 165 | } 166 | 167 | 168 | -------------------------------------------------------------------------------- /src/LLMProviders/Gemini/Responses/GeminiResponse.php: -------------------------------------------------------------------------------- 1 | content; 19 | } 20 | 21 | public function getModel(): ?string 22 | { 23 | return $this->model; 24 | } 25 | 26 | public function getUsage(): ?object 27 | { 28 | return $this->usage; 29 | } 30 | 31 | public function getRawResponse(): ?object 32 | { 33 | return $this->rawResponse; 34 | } 35 | 36 | public function toArray(): array 37 | { 38 | return [ 39 | 'content' => $this->content, 40 | 'model' => $this->model, 41 | 'usage' => $this->usage, 42 | ]; 43 | } 44 | 45 | public function toJson(): string 46 | { 47 | return json_encode($this->toArray()); 48 | } 49 | 50 | public function jsonSerialize(): array 51 | { 52 | return $this->toArray(); 53 | } 54 | } 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/LLMProviders/OpenAI/OpenAIProvider.php: -------------------------------------------------------------------------------- 1 | client = \OpenAI::client($apiKey); 23 | $this->maxRetries = $maxRetries; 24 | $this->retryDelay = $retryDelay; 25 | } 26 | 27 | public function generateResponse( 28 | string $modelName, 29 | array $messages, 30 | ?float $temperature = null, 31 | ?int $maxTokens = null, 32 | ?array $stop = null, 33 | ?float $topP = null, 34 | ?float $frequencyPenalty = null, 35 | ?float $presencePenalty = null, 36 | ?array $logitBias = null, 37 | ?string $user = null, 38 | ?bool $jsonMode = false, 39 | bool $fullResponse = false 40 | ): OpenAIResponse { 41 | $parameters = $this->buildParameters( 42 | modelName: $modelName, 43 | messages: $messages, 44 | temperature: $temperature, 45 | maxTokens: $maxTokens, 46 | stop: $stop, 47 | topP: $topP, 48 | frequencyPenalty: $frequencyPenalty, 49 | presencePenalty: $presencePenalty, 50 | logitBias: $logitBias, 51 | user: $user, 52 | jsonMode: $jsonMode 53 | ); 54 | 55 | return $this->executeWithRetry(function () use ($parameters, $fullResponse) { 56 | $response = $this->client->chat()->create($parameters); 57 | 58 | return $this->createResponse($response, $fullResponse); 59 | }); 60 | } 61 | 62 | /** 63 | * Build the parameters array for the OpenAI API request 64 | */ 65 | private function buildParameters( 66 | string $modelName, 67 | array $messages, 68 | ?float $temperature, 69 | ?int $maxTokens, 70 | ?array $stop, 71 | ?float $topP, 72 | ?float $frequencyPenalty, 73 | ?float $presencePenalty, 74 | ?array $logitBias, 75 | ?string $user, 76 | ?bool $jsonMode 77 | ): array { 78 | $parameters = [ 79 | 'model' => $modelName, 80 | 'messages' => $messages, 81 | ]; 82 | 83 | $optionalParameters = [ 84 | 'temperature' => $temperature, 85 | 'max_tokens' => $maxTokens, 86 | 'stop' => $stop, 87 | 'top_p' => $topP, 88 | 'frequency_penalty' => $frequencyPenalty, 89 | 'presence_penalty' => $presencePenalty, 90 | 'logit_bias' => $logitBias, 91 | 'user' => $user, 92 | ]; 93 | 94 | foreach ($optionalParameters as $key => $value) { 95 | if ($value !== null) { 96 | $parameters[$key] = $value; 97 | } 98 | } 99 | 100 | if ($jsonMode) { 101 | $parameters['response_format'] = ['type' => 'json_object']; 102 | } 103 | 104 | return $parameters; 105 | } 106 | 107 | /** 108 | * Create an OpenAIResponse object from the API response 109 | */ 110 | private function createResponse(object $response, bool $fullResponse): OpenAIResponse 111 | { 112 | if ($fullResponse) { 113 | return new OpenAIResponse( 114 | content: $response->choices[0]->message->content, 115 | model: $response->model, 116 | usage: $response->usage, 117 | rawResponse: $response 118 | ); 119 | } 120 | 121 | return new OpenAIResponse( 122 | content: $response->choices[0]->message->content 123 | ); 124 | } 125 | 126 | /** 127 | * Execute a function with retry logic 128 | * 129 | * @template T 130 | * @param callable(): T $callback 131 | * @return T 132 | * @throws ErrorException 133 | */ 134 | private function executeWithRetry(callable $callback) 135 | { 136 | $attempt = 0; 137 | $lastException = null; 138 | 139 | while ($attempt < $this->maxRetries) { 140 | try { 141 | return $callback(); 142 | } catch (ErrorException $e) { 143 | $lastException = $e; 144 | $attempt++; 145 | 146 | if ($attempt < $this->maxRetries) { 147 | Log::warning("OpenAI API request failed, retrying...", [ 148 | 'attempt' => $attempt, 149 | 'error' => $e->getMessage() 150 | ]); 151 | sleep($this->retryDelay); 152 | } 153 | } 154 | } 155 | 156 | Log::error("OpenAI API request failed after {$this->maxRetries} attempts", [ 157 | 'error' => $lastException?->getMessage() 158 | ]); 159 | 160 | throw $lastException; 161 | } 162 | } -------------------------------------------------------------------------------- /src/LLMProviders/OpenAI/Responses/OpenAIResponse.php: -------------------------------------------------------------------------------- 1 | content; 22 | } 23 | 24 | /** 25 | * Get the model used for the response 26 | */ 27 | public function getModel(): ?string 28 | { 29 | return $this->model; 30 | } 31 | 32 | /** 33 | * Get the token usage information 34 | */ 35 | public function getUsage(): ?object 36 | { 37 | return $this->usage; 38 | } 39 | 40 | /** 41 | * Get the raw response from the API 42 | */ 43 | public function getRawResponse(): ?object 44 | { 45 | return $this->rawResponse; 46 | } 47 | 48 | /** 49 | * Convert the response to an array 50 | */ 51 | public function toArray(): array 52 | { 53 | return [ 54 | 'content' => $this->content, 55 | 'model' => $this->model, 56 | 'usage' => $this->usage, 57 | ]; 58 | } 59 | 60 | /** 61 | * Convert the response to JSON 62 | */ 63 | public function toJson(): string 64 | { 65 | return json_encode($this->toArray()); 66 | } 67 | 68 | /** 69 | * Specify data which should be serialized to JSON 70 | */ 71 | public function jsonSerialize(): array 72 | { 73 | return $this->toArray(); 74 | } 75 | } -------------------------------------------------------------------------------- /src/LaraUtilXServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->bind('AccessLog', AccessLog::class); 34 | 35 | // Register base LLM Provider interface with provider selection 36 | $this->app->bind(LLMProviderInterface::class, function ($app) { 37 | $default = config('lara-util-x.llm.default_provider', 'openai'); 38 | 39 | if ($default === 'gemini') { 40 | return new GeminiProvider( 41 | apiKey: config('lara-util-x.gemini.api_key'), 42 | maxRetries: (int) config('lara-util-x.gemini.max_retries', 3), 43 | retryDelay: (int) config('lara-util-x.gemini.retry_delay', 2), 44 | baseUrl: (string) config('lara-util-x.gemini.base_url', 'https://generativelanguage.googleapis.com/v1beta') 45 | ); 46 | } 47 | 48 | if ($default === 'claude') { 49 | return new ClaudeProvider( 50 | apiKey: config('lara-util-x.claude.api_key'), 51 | maxRetries: (int) config('lara-util-x.claude.max_retries', 3), 52 | retryDelay: (int) config('lara-util-x.claude.retry_delay', 2), 53 | baseUrl: (string) config('lara-util-x.claude.base_url', 'https://api.anthropic.com') 54 | ); 55 | } 56 | 57 | return new OpenAIProvider( 58 | apiKey: config('lara-util-x.openai.api_key'), 59 | maxRetries: (int) config('lara-util-x.openai.max_retries', 3), 60 | retryDelay: (int) config('lara-util-x.openai.retry_delay', 2) 61 | ); 62 | }); 63 | } 64 | 65 | /** 66 | * Bootstrap services. 67 | */ 68 | public function boot(): void 69 | { 70 | // Publish Service Provider 71 | $this->publishes([ 72 | __DIR__ . '/LaraUtilXServiceProvider.php' => app_path('Providers/LaraUtilXServiceProvider.php'), 73 | ], 'lara-util-x'); 74 | 75 | // Load migrations 76 | $this->loadMigrationsFrom(__DIR__ . '/../database/migrations'); 77 | 78 | // Publish configs 79 | $this->publishes([ 80 | __DIR__ . '/../config/lara-util-x.php' => config_path('lara-util-x.php'), 81 | ], 'lara-util-x-config'); 82 | 83 | $this->publishes([ 84 | __DIR__ . '/../config/feature-toggles.php' => config_path('feature-toggles.php'), 85 | ], 'lara-util-x-feature-toggles'); 86 | 87 | $this->mergeConfigFrom(__DIR__ . '/../config/lara-util-x.php', 'lara-util-x'); 88 | 89 | // Publish migrations 90 | $this->publishes([ 91 | __DIR__ . '/../database/migrations' => database_path('migrations'), 92 | ], 'lara-util-x-migrations'); 93 | 94 | // Publish models 95 | $this->publishes([ 96 | __DIR__ . '\Models' => app_path('Models'), 97 | ], 'lara-util-x-models'); 98 | 99 | // Publish traits 100 | $this->publishes([ 101 | __DIR__ . '/Traits/ApiResponseTrait.php' => app_path('Traits/ApiResponseTrait.php'), 102 | ], 'lara-util-x-api-response-trait'); 103 | 104 | // Publish validation rules 105 | $this->publishValidationRule(); 106 | 107 | $this->loadClass(ApiResponseTrait::class); 108 | $this->loadClass(FileProcessingTrait::class); 109 | 110 | // Publish utilities 111 | $this->publishUtility('CachingUtil', 'caching'); 112 | $this->publishUtility('ConfigUtil', 'config'); 113 | $this->publishUtility('SchedulerUtil', 'scheduler'); 114 | $this->publishUtility('QueryParameterUtil', 'query-parameter'); 115 | $this->publishUtility('RateLimiterUtil', 'rate-limiter'); 116 | $this->publishUtility('PaginationUtil', 'paginator'); 117 | $this->publishUtility('FilteringUtil', 'filtering'); 118 | $this->publishUtility('LoggingUtil', 'logging'); 119 | 120 | // Load utilities 121 | $classes = [ 122 | ConfigUtil::class, 123 | SchedulerUtil::class, 124 | QueryParameterUtil::class, 125 | RateLimiterUtil::class, 126 | PaginationUtil::class, 127 | FilteringUtil::class, 128 | FeatureToggleUtil::class, 129 | LoggingUtil::class 130 | ]; 131 | 132 | $this->loadUtilityClasses($classes); 133 | $this->loadCachingUtility(); 134 | 135 | // Register middleware 136 | $this->app['router']->aliasMiddleware('access.log', AccessLogMiddleware::class); 137 | 138 | // Register custom validation rules 139 | $this->registerValidationRules(); 140 | } 141 | 142 | 143 | /** 144 | * Dynamically load the given class. 145 | * 146 | * @param string $class 147 | */ 148 | private function loadClass(string $class) 149 | { 150 | $this->app->bind($class, function () use ($class) { 151 | return new $class(); 152 | }); 153 | } 154 | 155 | /** 156 | * Dynamically load the given utility classes. 157 | * 158 | * @param array $classes 159 | */ 160 | private function loadUtilityClasses(array $classes) 161 | { 162 | foreach ($classes as $class) { 163 | if ($class === RateLimiterUtil::class) { 164 | $this->loadRateLimiterUtility(); 165 | } else { 166 | $this->app->bind($class, function () use ($class) { 167 | return new $class(); 168 | }); 169 | } 170 | } 171 | } 172 | 173 | /** 174 | * Load the caching utility with configured options. 175 | */ 176 | private function loadCachingUtility() 177 | { 178 | $config = config('lara-util-x.cache'); 179 | 180 | $this->app->bind(CachingUtil::class, function () use ($config) { 181 | return new CachingUtil($config['default_expiration'], $config['default_tags']); 182 | }); 183 | } 184 | 185 | /** 186 | * Load the rate limiter utility with dependency injection. 187 | */ 188 | private function loadRateLimiterUtility() 189 | { 190 | $this->app->bind(RateLimiterUtil::class, function ($app) { 191 | return new RateLimiterUtil($app->make('cache.store')); 192 | }); 193 | } 194 | 195 | private function publishUtility(string $utility, string $name) 196 | { 197 | $this->publishes([ 198 | __DIR__ . '/Utilities/' . $utility . '.php' => app_path('Utilities/' . $utility . '.php'), 199 | ], 'lara-util-x-' . $name); 200 | } 201 | 202 | /** 203 | * Register custom validation rules. 204 | */ 205 | private function registerValidationRules(): void 206 | { 207 | Validator::extend('reject_common_passwords', function ($attribute, $value, $parameters, $validator) { 208 | $rule = new RejectCommonPasswords(); 209 | $failed = false; 210 | $rule->validate($attribute, $value, function ($message) use (&$failed) { 211 | $failed = true; 212 | }); 213 | return !$failed; 214 | }, 'The :attribute contains a common password that is not allowed.'); 215 | 216 | Validator::replacer('reject_common_passwords', function ($message, $attribute, $rule, $parameters) { 217 | return str_replace(':attribute', $attribute, $message); 218 | }); 219 | } 220 | 221 | /** 222 | * Publish validation rules with correct namespace for app directory. 223 | */ 224 | private function publishValidationRule(): void 225 | { 226 | $this->publishes([ 227 | __DIR__ . '/Rules/RejectCommonPasswords_App.php' => app_path('Rules/RejectCommonPasswords.php'), 228 | ], 'lara-util-x-validation-rules'); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/Models/AccessLog.php: -------------------------------------------------------------------------------- 1 | commonPasswords)) { 253 | $fail('The :attribute contains a common password that is not allowed.'); 254 | } 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /src/Traits/ApiResponseTrait.php: -------------------------------------------------------------------------------- 1 | json([ 20 | 'success' => true, 21 | 'message' => $message, 22 | 'data' => $data, 23 | 'meta' => $meta, 24 | ], $statusCode); 25 | } 26 | 27 | /** 28 | * Send a standardized error response with optional debug data. 29 | */ 30 | protected function errorResponse( 31 | string $message = 'Something went wrong.', 32 | int $statusCode = 500, 33 | array $errors = [], 34 | mixed $debug = null 35 | ): JsonResponse { 36 | $response = [ 37 | 'success' => false, 38 | 'message' => $message, 39 | 'errors' => $errors, 40 | ]; 41 | 42 | if (config('app.debug') && $debug !== null) { 43 | $response['debug'] = $debug; 44 | } 45 | 46 | return response()->json($response, $statusCode); 47 | } 48 | 49 | /** 50 | * Handle exception responses (useful for centralized error handling). 51 | */ 52 | protected function exceptionResponse(\Throwable $e, int $statusCode = 500): JsonResponse 53 | { 54 | Log::error($e->getMessage(), [ 55 | 'exception' => $e, 56 | 'file' => $e->getFile(), 57 | 'line' => $e->getLine(), 58 | ]); 59 | 60 | return $this->errorResponse( 61 | 'Internal server error.', 62 | $statusCode, 63 | [], 64 | [ 65 | 'exception' => get_class($e), 66 | 'message' => $e->getMessage(), 67 | 'trace' => $e->getTrace(), 68 | ] 69 | ); 70 | } 71 | 72 | /** 73 | * Send a paginated response with meta info. 74 | */ 75 | protected function paginatedResponse($paginator, string $message = 'Data fetched successfully.'): JsonResponse 76 | { 77 | return $this->successResponse( 78 | $paginator->items(), 79 | $message, 80 | 200, 81 | [ 82 | 'pagination' => [ 83 | 'total' => $paginator->total(), 84 | 'count' => $paginator->count(), 85 | 'per_page' => $paginator->perPage(), 86 | 'current_page' => $paginator->currentPage(), 87 | 'total_pages' => $paginator->lastPage(), 88 | ] 89 | ] 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Traits/Auditable.php: -------------------------------------------------------------------------------- 1 | getAttributes()); 15 | }); 16 | 17 | static::updated(function (Model $model) { 18 | self::logAudit($model, 'updated', $model->getOriginal(), $model->getChanges()); 19 | }); 20 | 21 | static::deleted(function (Model $model) { 22 | self::logAudit($model, 'deleted', $model->getOriginal()); 23 | }); 24 | } 25 | 26 | private static function logAudit(Model $model, string $event, array $oldValues = [], array $newValues = []): void 27 | { 28 | DB::table('model_audits')->insert([ 29 | 'model_type' => get_class($model), 30 | 'model_id' => $model->getKey(), 31 | 'event' => $event, 32 | 'old_values' => json_encode($oldValues), 33 | 'new_values' => json_encode($newValues), 34 | 'user_id' => Auth::id(), 35 | 'created_at' => now(), 36 | 'updated_at' => now(), 37 | ]); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Traits/FileProcessingTrait.php: -------------------------------------------------------------------------------- 1 | getClientOriginalName(); 40 | 41 | $file->storeAs($directory, $filename); 42 | 43 | return $filename; 44 | } 45 | 46 | /** 47 | * Upload multiple files. 48 | * 49 | * @param array $files 50 | * @param string $directory 51 | * @return array 52 | */ 53 | public function uploadFiles(array $files, string $directory = 'uploads') 54 | { 55 | $filenames = []; 56 | 57 | foreach ($files as $file) { 58 | $filenames[] = $this->uploadFile($file, $directory); 59 | } 60 | 61 | return $filenames; 62 | } 63 | 64 | /** 65 | * Delete a file. 66 | * 67 | * @param string $filename 68 | * @param string $directory 69 | * @return void 70 | */ 71 | public function deleteFile(string $filename, string $directory = 'uploads') 72 | { 73 | Storage::delete($directory . '/' . $filename); 74 | } 75 | 76 | /** 77 | * Delete multiple files. 78 | * 79 | * @param array $filenames 80 | * @param string $directory 81 | * @return void 82 | */ 83 | public function deleteFiles(array $filenames, string $directory = 'uploads') 84 | { 85 | foreach ($filenames as $filename) { 86 | $this->deleteFile($filename, $directory); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Utilities/CachingUtil.php: -------------------------------------------------------------------------------- 1 | defaultExpiration = $defaultExpiration; 15 | $this->defaultTags = $defaultTags; 16 | } 17 | 18 | /** 19 | * Cache data with configurable options. 20 | * 21 | * @param string $key 22 | * @param mixed $data 23 | * @param int $minutes 24 | * @param array $tags 25 | * @return mixed 26 | */ 27 | public function cache(string $key, mixed $data, int $minutes = null, array $tags = null) 28 | { 29 | // Use constructor defaults if parameters are null 30 | $minutes = $minutes ?? $this->defaultExpiration; 31 | $tags = $tags ?? $this->defaultTags; 32 | 33 | // Convert minutes to seconds for Cache::put() 34 | $seconds = $minutes * 60; 35 | 36 | // Try to use tags if the store supports it and tags are provided 37 | if (Cache::getStore() instanceof \Illuminate\Cache\TaggableStore && !empty($tags)) { 38 | try { 39 | Cache::tags($tags)->put($key, $data, $seconds); 40 | } catch (\Exception $e) { 41 | // Fallback to regular cache if tags fail 42 | Cache::put($key, $data, $seconds); 43 | } 44 | } else { 45 | Cache::put($key, $data, $seconds); 46 | } 47 | 48 | return $data; 49 | } 50 | 51 | /** 52 | * Retrieve cached data. 53 | * 54 | * @param string $key 55 | * @param mixed $default 56 | * @return mixed 57 | */ 58 | public function get(string $key, mixed $default = null) 59 | { 60 | return Cache::get($key, $default); 61 | } 62 | 63 | /** 64 | * Forget cached data. 65 | * 66 | * @param string $key 67 | * @return void 68 | */ 69 | public function forget(string $key) 70 | { 71 | Cache::forget($key); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Utilities/ConfigUtil.php: -------------------------------------------------------------------------------- 1 | getAllAppSettings(); 23 | return $settings[$key] ?? null; 24 | } 25 | 26 | if (Storage::exists($filePath)) { 27 | $settingsJson = Storage::get($filePath); 28 | return json_decode($settingsJson, true); 29 | } 30 | 31 | return []; 32 | } 33 | 34 | /** 35 | * Get a specific dynamic configuration setting. 36 | * 37 | * @param string $key 38 | * @return mixed 39 | */ 40 | public function getSetting(string $key) 41 | { 42 | $settings = $this->getAllSettings(); 43 | 44 | return $settings[$key] ?? null; 45 | } 46 | 47 | /** 48 | * Set or update a dynamic configuration setting. 49 | * 50 | * @param string $key 51 | * @param mixed $value 52 | * @return void 53 | */ 54 | public function setSetting(string $key, mixed $value) 55 | { 56 | $settings = $this->getAllSettings(); 57 | $settings[$key] = $value; 58 | 59 | $filePath = storage_path('app/config/settings.json'); 60 | Storage::put($filePath, json_encode($settings)); 61 | } 62 | 63 | 64 | /** 65 | * Get all application settings. 66 | * 67 | * @return array 68 | */ 69 | public function getAllAppSettings() 70 | { 71 | return config('app'); 72 | } 73 | } -------------------------------------------------------------------------------- /src/Utilities/FeatureToggleUtil.php: -------------------------------------------------------------------------------- 1 | user(); 42 | 43 | return $user ? 'user.' . $user->id : 'environment.' . app()->environment(); 44 | } 45 | 46 | /** 47 | * Ensure the feature-toggles.php configuration file exists, if not, create it. 48 | */ 49 | private static function ensureConfigFileExists() 50 | { 51 | $configPath = config_path('feature-toggles.php'); 52 | 53 | // Create the configuration file if it doesn't exist 54 | if (!File::exists($configPath)) { 55 | $sourcePath = __DIR__ . '/../../config/feature-toggles.php'; 56 | if (File::exists($sourcePath)) { 57 | File::copy($sourcePath, $configPath); 58 | } 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /src/Utilities/FilteringUtil.php: -------------------------------------------------------------------------------- 1 | filter(function ($item) use ($name, $operator, $value) { 21 | switch ($operator) { 22 | case 'equals': 23 | return data_get($item, $name) == $value; 24 | case 'not_equals': 25 | return data_get($item, $name) != $value; 26 | case 'contains': 27 | return stripos(data_get($item, $name), $value) !== false; 28 | case 'not_contains': 29 | return stripos(data_get($item, $name), $value) === false; 30 | case 'starts_with': 31 | return stripos(data_get($item, $name), $value) === 0; 32 | case 'ends_with': 33 | return stripos(data_get($item, $name), $value) === strlen(data_get($item, $name)) - strlen($value); 34 | default: 35 | return false; 36 | } 37 | }); 38 | } 39 | } -------------------------------------------------------------------------------- /src/Utilities/LoggingUtil.php: -------------------------------------------------------------------------------- 1 | setFormatter(new JsonFormatter()); 32 | 33 | 34 | self::$customLogger = new Logger('custom'); 35 | self::$customLogger->pushHandler($handler); 36 | } 37 | 38 | return self::$customLogger; 39 | } 40 | 41 | /** 42 | * Log a message with context and formatting. 43 | * 44 | * @param LogLevel $level Log level (debug, info, warning, error, critical) 45 | * @param string $message Log message 46 | * @param array $context Additional context data 47 | * @param string|null $channel Log channel (default, single, daily, custom, etc.) 48 | */ 49 | public static function log(LogLevel $level, string $message, array $context = [], ?string $channel = null): void 50 | { 51 | $logger = self::getLogger($channel); 52 | $context['timestamp'] = now()->toDateTimeString(); 53 | $context['env'] = Config::get('app.env'); 54 | 55 | $logger->{$level->value}($message, $context); 56 | } 57 | 58 | public static function info(string $message, array $context = [], ?string $channel = null): void 59 | { 60 | self::log(LogLevel::Info, $message, $context, $channel); 61 | } 62 | 63 | public static function debug(string $message, array $context = [], ?string $channel = null): void 64 | { 65 | self::log(LogLevel::Debug, $message, $context, $channel); 66 | } 67 | 68 | public static function warning(string $message, array $context = [], ?string $channel = null): void 69 | { 70 | self::log(LogLevel::Warning, $message, $context, $channel); 71 | } 72 | 73 | public static function error(string $message, array $context = [], ?string $channel = null): void 74 | { 75 | self::log(LogLevel::Error, $message, $context, $channel); 76 | } 77 | 78 | public static function critical(string $message, array $context = [], ?string $channel = null): void 79 | { 80 | self::log(LogLevel::Critical, $message, $context, $channel); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Utilities/PaginationUtil.php: -------------------------------------------------------------------------------- 1 | input('page', 1)); 43 | return $query->paginate($perPage, ['*'], 'page', $page)->appends($options); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Utilities/QueryParameterUtil.php: -------------------------------------------------------------------------------- 1 | has($param)) { 22 | $queryParams[$param] = $request->input($param); 23 | } 24 | } 25 | 26 | return $queryParams; 27 | } 28 | } -------------------------------------------------------------------------------- /src/Utilities/RateLimiterUtil.php: -------------------------------------------------------------------------------- 1 | rateLimiter = new RateLimiter($cache); 27 | } 28 | 29 | /** 30 | * Attempt to hit the given rate limiter. 31 | * 32 | * @param string $key 33 | * @param int $maxAttempts 34 | * @param int $decayMinutes 35 | * @return bool 36 | */ 37 | public function attempt(string $key, int $maxAttempts, int $decayMinutes): bool 38 | { 39 | if ($this->rateLimiter->tooManyAttempts($key, $maxAttempts)) { 40 | return false; 41 | } 42 | 43 | $this->rateLimiter->hit($key, $decayMinutes * 60); 44 | 45 | return true; 46 | } 47 | 48 | /** 49 | * Get the number of attempts for the given key. 50 | * 51 | * @param string $key 52 | * @return int 53 | */ 54 | public function attempts(string $key): int 55 | { 56 | return $this->rateLimiter->attempts($key); 57 | } 58 | 59 | /** 60 | * Get the number of remaining attempts for the given key. 61 | * 62 | * @param string $key 63 | * @param int $maxAttempts 64 | * @return int 65 | */ 66 | public function remaining(string $key, int $maxAttempts): int 67 | { 68 | return $this->rateLimiter->remaining($key, $maxAttempts); 69 | } 70 | 71 | /** 72 | * Clear the hits and lockout timer for the given key. 73 | * 74 | * @param string $key 75 | * @return void 76 | */ 77 | public function clear(string $key): void 78 | { 79 | $this->rateLimiter->clear($key); 80 | } 81 | 82 | /** 83 | * Get the number of seconds until the "key" is accessible again. 84 | * 85 | * @param string $key 86 | * @return int 87 | */ 88 | public function availableIn(string $key): int 89 | { 90 | return $this->rateLimiter->availableIn($key); 91 | } 92 | 93 | /** 94 | * Determine if the given key has been "accessed" too many times. 95 | * 96 | * @param string $key 97 | * @param int $maxAttempts 98 | * @return bool 99 | */ 100 | public function tooManyAttempts(string $key, int $maxAttempts): bool 101 | { 102 | return $this->rateLimiter->tooManyAttempts($key, $maxAttempts); 103 | } 104 | 105 | /** 106 | * Increment the counter for a given key for a given decay time. 107 | * 108 | * @param string $key 109 | * @param int $decaySeconds 110 | * @return int 111 | */ 112 | public function hit(string $key, int $decaySeconds = 60): int 113 | { 114 | return $this->rateLimiter->hit($key, $decaySeconds); 115 | } 116 | 117 | /** 118 | * Get the underlying rate limiter instance. 119 | * 120 | * @return \Illuminate\Cache\RateLimiter 121 | */ 122 | public function getRateLimiter(): RateLimiter 123 | { 124 | return $this->rateLimiter; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Utilities/SchedulerUtil.php: -------------------------------------------------------------------------------- 1 | events(), true)); 22 | 23 | return collect($schedule->events())->map(function (Event $event) { 24 | return [ 25 | 'command' => $event->command, 26 | 'expression' => $event->expression, 27 | 'description' => $event->description, 28 | 'next_run' => $event->nextRunDate(), 29 | 'is_due' => $this->isDue($event), 30 | 'is_running' => $event->isRunning(), 31 | 'output' => $event->output, 32 | ]; 33 | })->toArray(); 34 | } 35 | 36 | /** 37 | * Check if any scheduled tasks are overdue. 38 | * 39 | * @param Event $event 40 | * @return bool 41 | */ 42 | private function isDue(Event $event) 43 | { 44 | $nextRunDate = $event->getNextRunDate(); 45 | 46 | return $nextRunDate <= Carbon::now(); 47 | } 48 | 49 | /** 50 | * Check if any scheduled tasks are overdue. 51 | * 52 | * @return bool 53 | */ 54 | public function hasOverdueTasks() 55 | { 56 | $schedule = app(Schedule::class); 57 | 58 | return collect($schedule->events())->filter(function (Event $event) { 59 | return $this->isDue($event) && !$event->isRunning(); 60 | })->isNotEmpty(); 61 | } 62 | } -------------------------------------------------------------------------------- /tests/Feature/Traits/ApiResponseTraitFeatureTest.php: -------------------------------------------------------------------------------- 1 | [ 18 | ['id' => 1, 'name' => 'John Doe'], 19 | ['id' => 2, 'name' => 'Jane Smith'], 20 | ], 21 | ]; 22 | $message = 'Users retrieved successfully'; 23 | $meta = [ 24 | 'total' => 2, 25 | 'page' => 1, 26 | ]; 27 | 28 | $response = $this->successResponse($data, $message, 200, $meta); 29 | 30 | $this->assertInstanceOf(JsonResponse::class, $response); 31 | $this->assertEquals(200, $response->getStatusCode()); 32 | 33 | $responseData = $response->getData(true); 34 | $this->assertTrue($responseData['success']); 35 | $this->assertEquals($message, $responseData['message']); 36 | $this->assertEquals($data, $responseData['data']); 37 | $this->assertEquals($meta, $responseData['meta']); 38 | } 39 | 40 | public function test_error_response_integration() 41 | { 42 | $message = 'Validation failed'; 43 | $errors = [ 44 | 'email' => ['The email field is required.'], 45 | 'password' => ['The password field is required.'], 46 | ]; 47 | $debug = [ 48 | 'request_data' => ['email' => '', 'password' => ''], 49 | 'validation_rules' => ['email' => 'required', 'password' => 'required'], 50 | ]; 51 | 52 | $response = $this->errorResponse($message, 422, $errors, $debug); 53 | 54 | $this->assertInstanceOf(JsonResponse::class, $response); 55 | $this->assertEquals(422, $response->getStatusCode()); 56 | 57 | $responseData = $response->getData(true); 58 | $this->assertFalse($responseData['success']); 59 | $this->assertEquals($message, $responseData['message']); 60 | $this->assertEquals($errors, $responseData['errors']); 61 | $this->assertEquals($debug, $responseData['debug']); 62 | } 63 | 64 | public function test_paginated_response_integration() 65 | { 66 | // Create a mock paginator with realistic data 67 | $items = collect(range(1, 25))->map(function ($i) { 68 | return [ 69 | 'id' => $i, 70 | 'name' => "Item {$i}", 71 | 'created_at' => now()->subDays($i)->toDateTimeString(), 72 | ]; 73 | }); 74 | 75 | $paginator = new LengthAwarePaginator( 76 | $items->forPage(1, 10), 77 | 25, 78 | 10, 79 | 1, 80 | ['path' => '/api/items'] 81 | ); 82 | 83 | $response = $this->paginatedResponse($paginator, 'Items retrieved successfully'); 84 | 85 | $this->assertInstanceOf(JsonResponse::class, $response); 86 | $this->assertEquals(200, $response->getStatusCode()); 87 | 88 | $responseData = $response->getData(true); 89 | $this->assertTrue($responseData['success']); 90 | $this->assertEquals('Items retrieved successfully', $responseData['message']); 91 | $this->assertCount(10, $responseData['data']); 92 | $this->assertArrayHasKey('pagination', $responseData['meta']); 93 | 94 | $pagination = $responseData['meta']['pagination']; 95 | $this->assertEquals(25, $pagination['total']); 96 | $this->assertEquals(10, $pagination['count']); 97 | $this->assertEquals(10, $pagination['per_page']); 98 | $this->assertEquals(1, $pagination['current_page']); 99 | $this->assertEquals(3, $pagination['total_pages']); 100 | } 101 | 102 | public function test_exception_response_integration() 103 | { 104 | $exception = new \InvalidArgumentException('Invalid parameter provided'); 105 | $exception->setTrace([ 106 | ['file' => '/app/controllers/TestController.php', 'line' => 42], 107 | ['file' => '/app/routes/web.php', 'line' => 15], 108 | ]); 109 | 110 | $response = $this->exceptionResponse($exception, 400); 111 | 112 | $this->assertInstanceOf(JsonResponse::class, $response); 113 | $this->assertEquals(400, $response->getStatusCode()); 114 | 115 | $responseData = $response->getData(true); 116 | $this->assertFalse($responseData['success']); 117 | $this->assertEquals('Internal server error.', $responseData['message']); 118 | $this->assertArrayHasKey('debug', $responseData); 119 | $this->assertEquals(\InvalidArgumentException::class, $responseData['debug']['exception']); 120 | $this->assertEquals('Invalid parameter provided', $responseData['debug']['message']); 121 | $this->assertIsArray($responseData['debug']['trace']); 122 | } 123 | 124 | public function test_debug_mode_affects_error_response() 125 | { 126 | // Test with debug enabled 127 | Config::set('app.debug', true); 128 | $response = $this->errorResponse('Test error', 500, [], ['debug_info' => 'sensitive_data']); 129 | $responseData = $response->getData(true); 130 | $this->assertArrayHasKey('debug', $responseData); 131 | 132 | // Test with debug disabled 133 | Config::set('app.debug', false); 134 | $response = $this->errorResponse('Test error', 500, [], ['debug_info' => 'sensitive_data']); 135 | $responseData = $response->getData(true); 136 | $this->assertArrayNotHasKey('debug', $responseData); 137 | } 138 | 139 | public function test_response_headers_are_correct() 140 | { 141 | $response = $this->successResponse(['test' => 'data']); 142 | 143 | $this->assertEquals('application/json', $response->headers->get('Content-Type')); 144 | $this->assertNotNull($response->headers->get('Date')); 145 | } 146 | 147 | public function test_large_data_response_performance() 148 | { 149 | $largeData = array_fill(0, 1000, [ 150 | 'id' => 1, 151 | 'name' => 'Test Item', 152 | 'description' => 'This is a test item with some description', 153 | 'metadata' => [ 154 | 'created_at' => now()->toDateTimeString(), 155 | 'updated_at' => now()->toDateTimeString(), 156 | ], 157 | ]); 158 | 159 | $startTime = microtime(true); 160 | $response = $this->successResponse($largeData); 161 | $responseTime = microtime(true) - $startTime; 162 | 163 | $this->assertInstanceOf(JsonResponse::class, $response); 164 | $this->assertLessThan(1.0, $responseTime); // Should respond in less than 1 second 165 | 166 | $responseData = $response->getData(true); 167 | $this->assertCount(1000, $responseData['data']); 168 | } 169 | 170 | public function test_nested_data_integrity() 171 | { 172 | $nestedData = [ 173 | 'user' => [ 174 | 'id' => 123, 175 | 'profile' => [ 176 | 'name' => 'John Doe', 177 | 'settings' => [ 178 | 'theme' => 'dark', 179 | 'notifications' => [ 180 | 'email' => true, 181 | 'push' => false, 182 | ], 183 | ], 184 | ], 185 | ], 186 | 'permissions' => ['read', 'write'], 187 | ]; 188 | 189 | $response = $this->successResponse($nestedData); 190 | $responseData = $response->getData(true); 191 | 192 | $this->assertEquals($nestedData, $responseData['data']); 193 | $this->assertEquals(123, $responseData['data']['user']['id']); 194 | $this->assertEquals('John Doe', $responseData['data']['user']['profile']['name']); 195 | $this->assertEquals('dark', $responseData['data']['user']['profile']['settings']['theme']); 196 | $this->assertTrue($responseData['data']['user']['profile']['settings']['notifications']['email']); 197 | $this->assertFalse($responseData['data']['user']['profile']['settings']['notifications']['push']); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /tests/Feature/Utilities/CachingUtilFeatureTest.php: -------------------------------------------------------------------------------- 1 | cachingUtil = new CachingUtil(60, ['default']); 17 | Cache::flush(); 18 | } 19 | 20 | public function test_caching_workflow_integration() 21 | { 22 | $key = 'integration_test'; 23 | $data = ['user_id' => 123, 'name' => 'John Doe', 'email' => 'john@example.com']; 24 | 25 | // Cache the data 26 | $result = $this->cachingUtil->cache($key, $data); 27 | $this->assertEquals($data, $result); 28 | 29 | // Retrieve the data 30 | $retrieved = $this->cachingUtil->get($key); 31 | $this->assertEquals($data, $retrieved); 32 | 33 | // Verify it's actually cached 34 | $this->assertTrue(Cache::has($key)); 35 | 36 | // Update the data 37 | $updatedData = ['user_id' => 123, 'name' => 'John Smith', 'email' => 'john.smith@example.com']; 38 | $this->cachingUtil->cache($key, $updatedData); 39 | 40 | // Verify the update 41 | $retrieved = $this->cachingUtil->get($key); 42 | $this->assertEquals($updatedData, $retrieved); 43 | 44 | // Clear the cache 45 | $this->cachingUtil->forget($key); 46 | $this->assertFalse(Cache::has($key)); 47 | } 48 | 49 | public function test_caching_with_different_expiration_times() 50 | { 51 | $shortTermKey = 'short_term'; 52 | $longTermKey = 'long_term'; 53 | $data = ['test' => 'data']; 54 | 55 | // Cache with short expiration 56 | $this->cachingUtil->cache($shortTermKey, $data, 1); // 1 minute 57 | 58 | // Cache with long expiration 59 | $this->cachingUtil->cache($longTermKey, $data, 60); // 60 minutes 60 | 61 | // Both should be available immediately 62 | $this->assertTrue(Cache::has($shortTermKey)); 63 | $this->assertTrue(Cache::has($longTermKey)); 64 | 65 | // Verify data integrity 66 | $this->assertEquals($data, $this->cachingUtil->get($shortTermKey)); 67 | $this->assertEquals($data, $this->cachingUtil->get($longTermKey)); 68 | } 69 | 70 | public function test_caching_with_tags() 71 | { 72 | $userKey = 'user_123'; 73 | $productKey = 'product_456'; 74 | $userData = ['name' => 'John Doe']; 75 | $productData = ['name' => 'Test Product']; 76 | 77 | // Cache with different tags 78 | $this->cachingUtil->cache($userKey, $userData, null, ['users']); 79 | $this->cachingUtil->cache($productKey, $productData, null, ['products']); 80 | 81 | // Both should be cached 82 | $this->assertTrue(Cache::has($userKey)); 83 | $this->assertTrue(Cache::has($productKey)); 84 | 85 | // Verify data 86 | $this->assertEquals($userData, $this->cachingUtil->get($userKey)); 87 | $this->assertEquals($productData, $this->cachingUtil->get($productKey)); 88 | } 89 | 90 | public function test_caching_performance_with_large_data() 91 | { 92 | $key = 'large_data_test'; 93 | $largeData = array_fill(0, 1000, [ 94 | 'id' => 1, 95 | 'name' => 'Test Item', 96 | 'description' => 'This is a test item with some description', 97 | 'created_at' => now()->toDateTimeString(), 98 | 'updated_at' => now()->toDateTimeString(), 99 | ]); 100 | 101 | $startTime = microtime(true); 102 | 103 | // Cache large data 104 | $result = $this->cachingUtil->cache($key, $largeData); 105 | 106 | $cacheTime = microtime(true) - $startTime; 107 | 108 | $this->assertEquals($largeData, $result); 109 | $this->assertLessThan(1.0, $cacheTime); // Should cache in less than 1 second 110 | 111 | // Retrieve large data 112 | $startTime = microtime(true); 113 | $retrieved = $this->cachingUtil->get($key); 114 | $retrieveTime = microtime(true) - $startTime; 115 | 116 | $this->assertEquals($largeData, $retrieved); 117 | $this->assertLessThan(0.5, $retrieveTime); // Should retrieve in less than 0.5 seconds 118 | } 119 | 120 | public function test_caching_with_complex_data_structures() 121 | { 122 | $key = 'complex_data'; 123 | $complexData = [ 124 | 'user' => [ 125 | 'id' => 123, 126 | 'profile' => [ 127 | 'name' => 'John Doe', 128 | 'settings' => [ 129 | 'theme' => 'dark', 130 | 'notifications' => true, 131 | 'preferences' => [ 132 | 'language' => 'en', 133 | 'timezone' => 'UTC', 134 | ], 135 | ], 136 | ], 137 | ], 138 | 'permissions' => ['read', 'write', 'admin'], 139 | 'metadata' => [ 140 | 'created_at' => now()->toDateTimeString(), 141 | 'last_login' => now()->subHours(2)->toDateTimeString(), 142 | ], 143 | ]; 144 | 145 | // Cache complex data 146 | $result = $this->cachingUtil->cache($key, $complexData); 147 | $this->assertEquals($complexData, $result); 148 | 149 | // Retrieve and verify 150 | $retrieved = $this->cachingUtil->get($key); 151 | $this->assertEquals($complexData, $retrieved); 152 | 153 | // Verify nested data integrity 154 | $this->assertEquals(123, $retrieved['user']['id']); 155 | $this->assertEquals('John Doe', $retrieved['user']['profile']['name']); 156 | $this->assertEquals('dark', $retrieved['user']['profile']['settings']['theme']); 157 | $this->assertTrue($retrieved['user']['profile']['settings']['notifications']); 158 | $this->assertEquals(['read', 'write', 'admin'], $retrieved['permissions']); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # LaraUtilX Test Suite 2 | 3 | This directory contains comprehensive unit and feature tests for the LaraUtilX package. 4 | 5 | ## Test Structure 6 | 7 | ``` 8 | tests/ 9 | ├── TestCase.php # Base test case with Laravel setup 10 | ├── Unit/ # Unit tests for individual components 11 | │ ├── Enums/ 12 | │ │ └── LogLevelTest.php 13 | │ ├── Rules/ 14 | │ │ └── RejectCommonPasswordsTest.php 15 | │ ├── Traits/ 16 | │ │ ├── ApiResponseTraitTest.php 17 | │ │ └── FileProcessingTraitTest.php 18 | │ └── Utilities/ 19 | │ ├── CachingUtilTest.php 20 | │ ├── ConfigUtilTest.php 21 | │ ├── FeatureToggleUtilTest.php 22 | │ ├── FilteringUtilTest.php 23 | │ ├── LoggingUtilTest.php 24 | │ ├── PaginationUtilTest.php 25 | │ ├── QueryParameterUtilTest.php 26 | │ ├── RateLimiterUtilTest.php 27 | │ └── SchedulerUtilTest.php 28 | └── Feature/ # Integration tests 29 | ├── Traits/ 30 | │ └── ApiResponseTraitFeatureTest.php 31 | └── Utilities/ 32 | └── CachingUtilFeatureTest.php 33 | ``` 34 | 35 | ## Running Tests 36 | 37 | ### Prerequisites 38 | 39 | Make sure you have installed the development dependencies: 40 | 41 | ```bash 42 | composer install --dev 43 | ``` 44 | 45 | ### Run All Tests 46 | 47 | ```bash 48 | ./vendor/bin/phpunit 49 | ``` 50 | 51 | ### Run Specific Test Suites 52 | 53 | ```bash 54 | # Run only unit tests 55 | ./vendor/bin/phpunit --testsuite Unit 56 | 57 | # Run only feature tests 58 | ./vendor/bin/phpunit --testsuite Feature 59 | ``` 60 | 61 | ### Run Specific Test Classes 62 | 63 | ```bash 64 | # Run tests for a specific utility 65 | ./vendor/bin/phpunit tests/Unit/Utilities/CachingUtilTest.php 66 | 67 | # Run tests for a specific trait 68 | ./vendor/bin/phpunit tests/Unit/Traits/ApiResponseTraitTest.php 69 | ``` 70 | 71 | ### Run Tests with Coverage 72 | 73 | ```bash 74 | ./vendor/bin/phpunit --coverage-html coverage 75 | ``` 76 | 77 | ## Test Coverage 78 | 79 | The test suite provides comprehensive coverage for: 80 | 81 | - **Utilities**: All utility classes with their methods and edge cases 82 | - **Traits**: All traits with their functionality and integration 83 | - **Enums**: All enum values and behaviors 84 | - **Rules**: Validation rules with various input scenarios 85 | - **Feature Tests**: Integration scenarios and performance tests 86 | 87 | ## Test Categories 88 | 89 | ### Unit Tests 90 | - Test individual components in isolation 91 | - Mock external dependencies 92 | - Focus on specific functionality 93 | - Fast execution 94 | 95 | ### Feature Tests 96 | - Test component integration 97 | - Use real Laravel services where appropriate 98 | - Test end-to-end workflows 99 | - Performance and scalability tests 100 | 101 | ## Writing New Tests 102 | 103 | When adding new functionality to LaraUtilX, follow these guidelines: 104 | 105 | 1. **Create unit tests** for individual methods and classes 106 | 2. **Create feature tests** for integration scenarios 107 | 3. **Use descriptive test names** that explain what is being tested 108 | 4. **Follow the AAA pattern**: Arrange, Act, Assert 109 | 5. **Mock external dependencies** in unit tests 110 | 6. **Test edge cases** and error conditions 111 | 7. **Maintain high test coverage** (aim for 90%+) 112 | 113 | ## Test Data 114 | 115 | - Use factories for creating test data 116 | - Use realistic data structures 117 | - Test with both valid and invalid inputs 118 | - Include edge cases and boundary conditions 119 | 120 | ## Continuous Integration 121 | 122 | The test suite is designed to run in CI environments with: 123 | - SQLite in-memory database 124 | - Array cache driver 125 | - Sync queue driver 126 | - Minimal external dependencies 127 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | loadLaravelMigrations(); 16 | } 17 | 18 | protected function getPackageProviders($app) 19 | { 20 | return [ 21 | LaraUtilXServiceProvider::class, 22 | ]; 23 | } 24 | 25 | protected function getEnvironmentSetUp($app) 26 | { 27 | // Setup default database to use sqlite :memory: 28 | $app['config']->set('database.default', 'testing'); 29 | $app['config']->set('database.connections.testing', [ 30 | 'driver' => 'sqlite', 31 | 'database' => ':memory:', 32 | 'prefix' => '', 33 | ]); 34 | 35 | // Setup cache to use array driver 36 | $app['config']->set('cache.default', 'array'); 37 | 38 | // Setup queue to use sync driver 39 | $app['config']->set('queue.default', 'sync'); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/Unit/Enums/LogLevelTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('debug', LogLevel::Debug->value); 13 | $this->assertEquals('info', LogLevel::Info->value); 14 | $this->assertEquals('warning', LogLevel::Warning->value); 15 | $this->assertEquals('error', LogLevel::Error->value); 16 | $this->assertEquals('critical', LogLevel::Critical->value); 17 | } 18 | 19 | public function test_log_level_enum_can_be_used_in_switch() 20 | { 21 | $level = LogLevel::Info; 22 | 23 | $result = match ($level) { 24 | LogLevel::Debug => 'debug', 25 | LogLevel::Info => 'info', 26 | LogLevel::Warning => 'warning', 27 | LogLevel::Error => 'error', 28 | LogLevel::Critical => 'critical', 29 | }; 30 | 31 | $this->assertEquals('info', $result); 32 | } 33 | 34 | public function test_log_level_enum_can_be_serialized() 35 | { 36 | $level = LogLevel::Error; 37 | $serialized = serialize($level); 38 | $unserialized = unserialize($serialized); 39 | 40 | $this->assertEquals($level, $unserialized); 41 | } 42 | 43 | public function test_log_level_enum_can_be_compared() 44 | { 45 | $level1 = LogLevel::Info; 46 | $level2 = LogLevel::Info; 47 | $level3 = LogLevel::Error; 48 | 49 | $this->assertTrue($level1 === $level2); 50 | $this->assertFalse($level1 === $level3); 51 | } 52 | 53 | public function test_all_log_levels_are_available() 54 | { 55 | $expectedLevels = ['debug', 'info', 'warning', 'error', 'critical']; 56 | $actualLevels = array_map(fn($case) => $case->value, LogLevel::cases()); 57 | 58 | $this->assertEquals($expectedLevels, $actualLevels); 59 | } 60 | 61 | public function test_log_level_enum_has_string_value() 62 | { 63 | $level = LogLevel::Warning; 64 | 65 | $this->assertEquals('warning', $level->value); 66 | $this->assertIsString($level->value); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/Unit/Rules/RejectCommonPasswordsTest.php: -------------------------------------------------------------------------------- 1 | rule = new RejectCommonPasswords(); 18 | } 19 | 20 | public function test_rejects_common_passwords() 21 | { 22 | $commonPasswords = [ 23 | 'password', 24 | '123456', 25 | '123456789', 26 | 'qwerty', 27 | 'abc123', 28 | 'password123', 29 | 'admin', 30 | 'letmein', 31 | 'welcome', 32 | 'monkey', 33 | ]; 34 | 35 | foreach ($commonPasswords as $password) { 36 | $validator = ValidatorFacade::make( 37 | ['password' => $password], 38 | ['password' => [$this->rule]] 39 | ); 40 | 41 | $this->assertTrue($validator->fails()); 42 | $this->assertStringContainsString( 43 | 'common password that is not allowed', 44 | $validator->errors()->first('password') 45 | ); 46 | } 47 | } 48 | 49 | public function test_accepts_strong_passwords() 50 | { 51 | $strongPasswords = [ 52 | 'MyStr0ng!P@ssw0rd', 53 | 'ComplexP@ssw0rd123!', 54 | 'VerySecureP@ssw0rd2024', 55 | 'RandomString123!@#', 56 | 'AnotherSecureP@ssw0rd', 57 | ]; 58 | 59 | foreach ($strongPasswords as $password) { 60 | $validator = ValidatorFacade::make( 61 | ['password' => $password], 62 | ['password' => [$this->rule]] 63 | ); 64 | 65 | $this->assertFalse($validator->fails()); 66 | } 67 | } 68 | 69 | public function test_case_insensitive_rejection() 70 | { 71 | $commonPasswords = [ 72 | 'PASSWORD', 73 | 'Password', 74 | 'PaSsWoRd', 75 | 'ADMIN', 76 | 'Admin', 77 | 'AdMiN', 78 | ]; 79 | 80 | foreach ($commonPasswords as $password) { 81 | $validator = ValidatorFacade::make( 82 | ['password' => $password], 83 | ['password' => [$this->rule]] 84 | ); 85 | 86 | $this->assertTrue($validator->fails()); 87 | } 88 | } 89 | 90 | public function test_handles_whitespace() 91 | { 92 | $passwordsWithWhitespace = [ 93 | ' password', 94 | 'password ', 95 | ' password ', 96 | "\tpassword", 97 | "password\t", 98 | ]; 99 | 100 | foreach ($passwordsWithWhitespace as $password) { 101 | $validator = ValidatorFacade::make( 102 | ['password' => $password], 103 | ['password' => [$this->rule]] 104 | ); 105 | 106 | $this->assertTrue($validator->fails()); 107 | } 108 | } 109 | 110 | public function test_accepts_non_string_values() 111 | { 112 | $nonStringValues = [ 113 | null, 114 | 123, 115 | [], 116 | true, 117 | false, 118 | ]; 119 | 120 | foreach ($nonStringValues as $value) { 121 | $validator = ValidatorFacade::make( 122 | ['password' => $value], 123 | ['password' => [$this->rule]] 124 | ); 125 | 126 | $this->assertFalse($validator->fails()); 127 | } 128 | } 129 | 130 | public function test_rejects_numeric_sequences() 131 | { 132 | $numericSequences = [ 133 | '111111', 134 | '000000', 135 | '666666', 136 | '888888', 137 | '999999', 138 | '11111111', 139 | '00000000', 140 | '123321', 141 | '654321', 142 | ]; 143 | 144 | foreach ($numericSequences as $password) { 145 | $validator = ValidatorFacade::make( 146 | ['password' => $password], 147 | ['password' => [$this->rule]] 148 | ); 149 | 150 | $this->assertTrue($validator->fails()); 151 | } 152 | } 153 | 154 | public function test_rejects_keyboard_patterns() 155 | { 156 | $keyboardPatterns = [ 157 | 'qwerty', 158 | 'qwertyuiop', 159 | 'asdfghjkl', 160 | 'zxcvbnm', 161 | 'qazwsx', 162 | ]; 163 | 164 | foreach ($keyboardPatterns as $password) { 165 | $validator = ValidatorFacade::make( 166 | ['password' => $password], 167 | ['password' => [$this->rule]] 168 | ); 169 | 170 | $this->assertTrue($validator->fails()); 171 | } 172 | } 173 | 174 | public function test_rejects_common_words_with_numbers() 175 | { 176 | $commonWithNumbers = [ 177 | 'password1', 178 | 'password12', 179 | 'password123', 180 | 'admin1', 181 | 'admin12', 182 | 'admin123', 183 | 'user1', 184 | 'user12', 185 | 'user123', 186 | ]; 187 | 188 | foreach ($commonWithNumbers as $password) { 189 | $validator = ValidatorFacade::make( 190 | ['password' => $password], 191 | ['password' => [$this->rule]] 192 | ); 193 | 194 | $this->assertTrue($validator->fails()); 195 | } 196 | } 197 | 198 | public function test_accepts_mixed_case_strong_passwords() 199 | { 200 | $mixedCasePasswords = [ 201 | 'MyPassword123!', 202 | 'SecurePass2024', 203 | 'ComplexP@ssw0rd', 204 | 'StrongP@ssw0rd123', 205 | ]; 206 | 207 | foreach ($mixedCasePasswords as $password) { 208 | $validator = ValidatorFacade::make( 209 | ['password' => $password], 210 | ['password' => [$this->rule]] 211 | ); 212 | 213 | $this->assertFalse($validator->fails()); 214 | } 215 | } 216 | 217 | public function test_rule_implements_validation_rule_interface() 218 | { 219 | $this->assertInstanceOf( 220 | \Illuminate\Contracts\Validation\ValidationRule::class, 221 | $this->rule 222 | ); 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /tests/Unit/Traits/ApiResponseTraitTest.php: -------------------------------------------------------------------------------- 1 | 'data']; 17 | $message = 'Test success message'; 18 | $statusCode = 200; 19 | $meta = ['meta_key' => 'meta_value']; 20 | 21 | $response = $this->successResponse($data, $message, $statusCode, $meta); 22 | 23 | $this->assertInstanceOf(JsonResponse::class, $response); 24 | $this->assertEquals($statusCode, $response->getStatusCode()); 25 | 26 | $responseData = $response->getData(true); 27 | $this->assertTrue($responseData['success']); 28 | $this->assertEquals($message, $responseData['message']); 29 | $this->assertEquals($data, $responseData['data']); 30 | $this->assertEquals($meta, $responseData['meta']); 31 | } 32 | 33 | public function test_success_response_with_defaults() 34 | { 35 | $response = $this->successResponse(); 36 | 37 | $this->assertInstanceOf(JsonResponse::class, $response); 38 | $this->assertEquals(200, $response->getStatusCode()); 39 | 40 | $responseData = $response->getData(true); 41 | $this->assertTrue($responseData['success']); 42 | $this->assertEquals('Request successful.', $responseData['message']); 43 | $this->assertNull($responseData['data']); 44 | $this->assertEmpty($responseData['meta']); 45 | } 46 | 47 | public function test_can_send_error_response() 48 | { 49 | $message = 'Test error message'; 50 | $statusCode = 400; 51 | $errors = ['field' => ['Error message']]; 52 | $debug = ['debug_key' => 'debug_value']; 53 | 54 | $response = $this->errorResponse($message, $statusCode, $errors, $debug); 55 | 56 | $this->assertInstanceOf(JsonResponse::class, $response); 57 | $this->assertEquals($statusCode, $response->getStatusCode()); 58 | 59 | $responseData = $response->getData(true); 60 | $this->assertFalse($responseData['success']); 61 | $this->assertEquals($message, $responseData['message']); 62 | $this->assertEquals($errors, $responseData['errors']); 63 | 64 | // Debug should be present when app.debug is true (default in tests) 65 | if (config('app.debug')) { 66 | $this->assertArrayHasKey('debug', $responseData); 67 | $this->assertEquals($debug, $responseData['debug']); 68 | } 69 | } 70 | 71 | public function test_error_response_with_defaults() 72 | { 73 | $response = $this->errorResponse(); 74 | 75 | $this->assertInstanceOf(JsonResponse::class, $response); 76 | $this->assertEquals(500, $response->getStatusCode()); 77 | 78 | $responseData = $response->getData(true); 79 | $this->assertFalse($responseData['success']); 80 | $this->assertEquals('Something went wrong.', $responseData['message']); 81 | $this->assertEmpty($responseData['errors']); 82 | $this->assertArrayNotHasKey('debug', $responseData); 83 | } 84 | 85 | public function test_error_response_hides_debug_in_production() 86 | { 87 | Config::set('app.debug', false); 88 | 89 | $debug = ['debug_key' => 'debug_value']; 90 | $response = $this->errorResponse('Error', 500, [], $debug); 91 | 92 | $responseData = $response->getData(true); 93 | $this->assertArrayNotHasKey('debug', $responseData); 94 | } 95 | 96 | public function test_can_handle_exception_response() 97 | { 98 | $exception = new \Exception('Test exception message'); 99 | $statusCode = 500; 100 | 101 | // Mock Log facade 102 | Log::shouldReceive('error') 103 | ->with($exception->getMessage(), \Mockery::type('array')) 104 | ->once(); 105 | 106 | $response = $this->exceptionResponse($exception, $statusCode); 107 | 108 | $this->assertInstanceOf(JsonResponse::class, $response); 109 | $this->assertEquals($statusCode, $response->getStatusCode()); 110 | 111 | $responseData = $response->getData(true); 112 | $this->assertFalse($responseData['success']); 113 | $this->assertEquals('Internal server error.', $responseData['message']); 114 | 115 | // Debug should be present when app.debug is true (default in tests) 116 | if (config('app.debug')) { 117 | $this->assertArrayHasKey('debug', $responseData); 118 | $this->assertEquals(\Exception::class, $responseData['debug']['exception']); 119 | $this->assertEquals('Test exception message', $responseData['debug']['message']); 120 | } 121 | } 122 | 123 | public function test_can_send_paginated_response() 124 | { 125 | // Create a real paginator instance 126 | $items = collect([1, 2, 3]); 127 | $paginator = new \Illuminate\Pagination\LengthAwarePaginator( 128 | $items, 129 | 10, 130 | 5, 131 | 1, 132 | ['path' => '/test'] 133 | ); 134 | 135 | $message = 'Paginated data fetched'; 136 | $response = $this->paginatedResponse($paginator, $message); 137 | 138 | $this->assertInstanceOf(JsonResponse::class, $response); 139 | $this->assertEquals(200, $response->getStatusCode()); 140 | 141 | $responseData = $response->getData(true); 142 | $this->assertTrue($responseData['success']); 143 | $this->assertEquals($message, $responseData['message']); 144 | $this->assertEquals([1, 2, 3], $responseData['data']); 145 | $this->assertArrayHasKey('pagination', $responseData['meta']); 146 | 147 | $pagination = $responseData['meta']['pagination']; 148 | $this->assertEquals(10, $pagination['total']); 149 | $this->assertEquals(3, $pagination['count']); 150 | $this->assertEquals(5, $pagination['per_page']); 151 | $this->assertEquals(1, $pagination['current_page']); 152 | $this->assertEquals(2, $pagination['total_pages']); 153 | } 154 | 155 | public function test_paginated_response_with_default_message() 156 | { 157 | $items = collect([1, 2, 3]); 158 | $paginator = new \Illuminate\Pagination\LengthAwarePaginator( 159 | $items, 160 | 10, 161 | 5, 162 | 1, 163 | ['path' => '/test'] 164 | ); 165 | 166 | $response = $this->paginatedResponse($paginator); 167 | 168 | $responseData = $response->getData(true); 169 | $this->assertEquals('Data fetched successfully.', $responseData['message']); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /tests/Unit/Traits/FileProcessingTraitTest.php: -------------------------------------------------------------------------------- 1 | exists('uploads')) { 26 | Storage::disk('local')->deleteDirectory('uploads'); 27 | } 28 | 29 | parent::tearDown(); 30 | } 31 | 32 | public function test_can_get_existing_file() 33 | { 34 | $filename = 'test_file.txt'; 35 | $directory = 'uploads'; 36 | $content = 'Test file content'; 37 | 38 | // Create a test file 39 | Storage::disk('local')->put($directory . '/' . $filename, $content); 40 | 41 | $result = $this->getFile($filename, $directory); 42 | 43 | $this->assertEquals($content, $result); 44 | } 45 | 46 | public function test_returns_file_not_found_for_non_existent_file() 47 | { 48 | $filename = 'non_existent.txt'; 49 | $directory = 'uploads'; 50 | 51 | $result = $this->getFile($filename, $directory); 52 | 53 | $this->assertEquals('File not found', $result); 54 | } 55 | 56 | public function test_can_upload_single_file() 57 | { 58 | $file = UploadedFile::fake()->create('test.pdf', 100); 59 | $directory = 'uploads'; 60 | 61 | $filename = $this->uploadFile($file, $directory); 62 | 63 | $this->assertIsString($filename); 64 | $this->assertStringContainsString('test.pdf', $filename); 65 | $this->assertTrue(Storage::disk('local')->exists($directory . '/' . $filename)); 66 | } 67 | 68 | public function test_uploaded_file_has_unique_name() 69 | { 70 | $file1 = UploadedFile::fake()->create('test.pdf', 100); 71 | $file2 = UploadedFile::fake()->create('test.pdf', 100); 72 | $directory = 'uploads'; 73 | 74 | $filename1 = $this->uploadFile($file1, $directory); 75 | $filename2 = $this->uploadFile($file2, $directory); 76 | 77 | $this->assertNotEquals($filename1, $filename2); 78 | } 79 | 80 | public function test_can_upload_multiple_files() 81 | { 82 | $files = [ 83 | UploadedFile::fake()->create('file1.pdf', 100), 84 | UploadedFile::fake()->create('file2.pdf', 100), 85 | UploadedFile::fake()->create('file3.pdf', 100), 86 | ]; 87 | $directory = 'uploads'; 88 | 89 | $filenames = $this->uploadFiles($files, $directory); 90 | 91 | $this->assertIsArray($filenames); 92 | $this->assertCount(3, $filenames); 93 | 94 | foreach ($filenames as $filename) { 95 | $this->assertIsString($filename); 96 | $this->assertTrue(Storage::disk('local')->exists($directory . '/' . $filename)); 97 | } 98 | } 99 | 100 | public function test_can_delete_single_file() 101 | { 102 | $filename = 'test_delete.txt'; 103 | $directory = 'uploads'; 104 | $content = 'Test content'; 105 | 106 | // Create a test file 107 | Storage::disk('local')->put($directory . '/' . $filename, $content); 108 | $this->assertTrue(Storage::disk('local')->exists($directory . '/' . $filename)); 109 | 110 | // Delete the file 111 | $this->deleteFile($filename, $directory); 112 | 113 | $this->assertFalse(Storage::disk('local')->exists($directory . '/' . $filename)); 114 | } 115 | 116 | public function test_can_delete_multiple_files() 117 | { 118 | $filenames = ['file1.txt', 'file2.txt', 'file3.txt']; 119 | $directory = 'uploads'; 120 | 121 | // Create test files 122 | foreach ($filenames as $filename) { 123 | Storage::disk('local')->put($directory . '/' . $filename, 'content'); 124 | } 125 | 126 | // Verify files exist 127 | foreach ($filenames as $filename) { 128 | $this->assertTrue(Storage::disk('local')->exists($directory . '/' . $filename)); 129 | } 130 | 131 | // Delete all files 132 | $this->deleteFiles($filenames, $directory); 133 | 134 | // Verify files are deleted 135 | foreach ($filenames as $filename) { 136 | $this->assertFalse(Storage::disk('local')->exists($directory . '/' . $filename)); 137 | } 138 | } 139 | 140 | public function test_uses_default_directory_when_not_specified() 141 | { 142 | $file = UploadedFile::fake()->create('test.pdf', 100); 143 | 144 | $filename = $this->uploadFile($file); 145 | 146 | $this->assertTrue(Storage::disk('local')->exists('uploads/' . $filename)); 147 | } 148 | 149 | public function test_handles_empty_files_array() 150 | { 151 | $filenames = $this->uploadFiles([], 'uploads'); 152 | 153 | $this->assertIsArray($filenames); 154 | $this->assertEmpty($filenames); 155 | } 156 | 157 | public function test_handles_empty_delete_files_array() 158 | { 159 | // This should not throw an exception 160 | $this->deleteFiles([], 'uploads'); 161 | 162 | $this->assertTrue(true); // If we get here, no exception was thrown 163 | } 164 | 165 | public function test_upload_preserves_file_extension() 166 | { 167 | $file = UploadedFile::fake()->create('document.pdf', 100); 168 | $directory = 'uploads'; 169 | 170 | $filename = $this->uploadFile($file, $directory); 171 | 172 | $this->assertStringEndsWith('.pdf', $filename); 173 | } 174 | 175 | public function test_upload_handles_special_characters_in_filename() 176 | { 177 | $file = UploadedFile::fake()->create('test file with spaces.pdf', 100); 178 | $directory = 'uploads'; 179 | 180 | $filename = $this->uploadFile($file, $directory); 181 | 182 | $this->assertIsString($filename); 183 | $this->assertTrue(Storage::disk('local')->exists($directory . '/' . $filename)); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /tests/Unit/Utilities/CachingUtilTest.php: -------------------------------------------------------------------------------- 1 | cachingUtil = new CachingUtil(60, ['default']); 18 | Cache::flush(); 19 | } 20 | 21 | public function test_can_cache_data_with_default_expiration() 22 | { 23 | $key = 'test_key'; 24 | $data = ['test' => 'data']; 25 | 26 | // Mock Cache facade 27 | Cache::shouldReceive('getStore')->andReturn(new \Illuminate\Cache\ArrayStore()); 28 | Cache::shouldReceive('put')->with($key, $data, \Mockery::any())->andReturn(true); 29 | Cache::shouldReceive('get')->with($key, null)->andReturn($data); 30 | Cache::shouldReceive('get')->with($key)->andReturn($data); 31 | 32 | $result = $this->cachingUtil->cache($key, $data); 33 | 34 | $this->assertEquals($data, $result); 35 | $this->assertEquals($data, Cache::get($key)); 36 | } 37 | 38 | public function test_can_cache_data_with_custom_expiration() 39 | { 40 | $key = 'test_key_custom'; 41 | $data = ['test' => 'data']; 42 | $minutes = 30; 43 | 44 | // Mock Cache facade 45 | Cache::shouldReceive('getStore')->andReturn(new \Illuminate\Cache\ArrayStore()); 46 | Cache::shouldReceive('put')->with($key, $data, $minutes * 60)->andReturn(true); 47 | Cache::shouldReceive('get')->with($key, null)->andReturn($data); 48 | Cache::shouldReceive('get')->with($key)->andReturn($data); 49 | 50 | $result = $this->cachingUtil->cache($key, $data, $minutes); 51 | 52 | $this->assertEquals($data, $result); 53 | $this->assertEquals($data, Cache::get($key)); 54 | } 55 | 56 | public function test_can_cache_data_with_custom_tags() 57 | { 58 | $key = 'test_key_tags'; 59 | $data = ['test' => 'data']; 60 | $tags = ['custom', 'test']; 61 | 62 | $result = $this->cachingUtil->cache($key, $data, null, $tags); 63 | 64 | $this->assertEquals($data, $result); 65 | } 66 | 67 | public function test_can_retrieve_cached_data() 68 | { 69 | $key = 'test_key_get'; 70 | $data = ['test' => 'data']; 71 | 72 | // Mock Cache facade 73 | Cache::shouldReceive('getStore')->andReturn(new \Illuminate\Cache\ArrayStore()); 74 | Cache::shouldReceive('put')->with($key, $data, \Mockery::any())->andReturn(true); 75 | Cache::shouldReceive('get')->with($key, null)->andReturn($data); 76 | 77 | $this->cachingUtil->cache($key, $data); 78 | $result = $this->cachingUtil->get($key); 79 | 80 | $this->assertEquals($data, $result); 81 | } 82 | 83 | public function test_returns_default_when_key_not_found() 84 | { 85 | $key = 'non_existent_key'; 86 | $default = 'default_value'; 87 | 88 | // Mock Cache facade 89 | Cache::shouldReceive('get')->with($key, $default)->andReturn($default); 90 | 91 | $result = $this->cachingUtil->get($key, $default); 92 | 93 | $this->assertEquals($default, $result); 94 | } 95 | 96 | public function test_can_forget_cached_data() 97 | { 98 | $key = 'test_key_forget'; 99 | $data = ['test' => 'data']; 100 | 101 | // Mock Cache facade - first call returns data, after forget returns null 102 | Cache::shouldReceive('getStore')->andReturn(new \Illuminate\Cache\ArrayStore()); 103 | Cache::shouldReceive('put')->with($key, $data, \Mockery::any())->andReturn(true); 104 | Cache::shouldReceive('get')->with($key, null)->andReturn($data)->once(); 105 | Cache::shouldReceive('forget')->with($key)->andReturn(true); 106 | Cache::shouldReceive('get')->with($key, null)->andReturn(null)->once(); 107 | 108 | $this->cachingUtil->cache($key, $data); 109 | $this->assertEquals($data, $this->cachingUtil->get($key)); 110 | 111 | $this->cachingUtil->forget($key); 112 | $this->assertNull($this->cachingUtil->get($key)); 113 | } 114 | 115 | public function test_handles_taggable_store_gracefully() 116 | { 117 | $key = 'test_key_taggable'; 118 | $data = ['test' => 'data']; 119 | $tags = ['test']; 120 | 121 | // Mock Cache facade to avoid store issues 122 | Cache::shouldReceive('getStore')->andReturn(new \Illuminate\Cache\ArrayStore()); 123 | Cache::shouldReceive('put')->andReturn(true); 124 | 125 | $result = $this->cachingUtil->cache($key, $data, null, $tags); 126 | 127 | $this->assertEquals($data, $result); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /tests/Unit/Utilities/ConfigUtilTest.php: -------------------------------------------------------------------------------- 1 | configUtil = new ConfigUtil(); 18 | } 19 | 20 | public function test_can_get_all_app_settings() 21 | { 22 | $settings = $this->configUtil->getAllAppSettings(); 23 | 24 | $this->assertIsArray($settings); 25 | $this->assertArrayHasKey('name', $settings); 26 | $this->assertArrayHasKey('env', $settings); 27 | } 28 | 29 | public function test_can_get_specific_app_setting() 30 | { 31 | $appName = $this->configUtil->getAllSettings(null, 'name'); 32 | 33 | $this->assertIsString($appName); 34 | } 35 | 36 | public function test_can_get_setting_from_existing_file() 37 | { 38 | // Create a test settings file 39 | $testSettings = ['test_key' => 'test_value', 'another_key' => 'another_value']; 40 | $filePath = 'test_settings.json'; 41 | 42 | Storage::put($filePath, json_encode($testSettings)); 43 | 44 | $settings = $this->configUtil->getAllSettings($filePath); 45 | 46 | $this->assertEquals($testSettings, $settings); 47 | 48 | // Clean up 49 | Storage::delete($filePath); 50 | } 51 | 52 | public function test_returns_empty_array_for_non_existent_file() 53 | { 54 | $settings = $this->configUtil->getAllSettings('non_existent_file.json'); 55 | 56 | $this->assertEquals([], $settings); 57 | } 58 | 59 | public function test_returns_null_for_non_existent_setting() 60 | { 61 | $setting = $this->configUtil->getSetting('non_existent_key'); 62 | 63 | $this->assertNull($setting); 64 | } 65 | 66 | public function test_can_set_and_get_dynamic_setting() 67 | { 68 | $key = 'dynamic_test_key'; 69 | $value = 'dynamic_test_value'; 70 | 71 | // Mock Storage to avoid file system issues 72 | Storage::shouldReceive('put') 73 | ->with(\Mockery::type('string'), \Mockery::type('string')) 74 | ->andReturn(true); 75 | 76 | $this->configUtil->setSetting($key, $value); 77 | 78 | // Test passes if no exception is thrown 79 | $this->assertTrue(true); 80 | } 81 | 82 | public function test_can_update_existing_setting() 83 | { 84 | $key = 'update_test_key'; 85 | $initialValue = 'initial_value'; 86 | $updatedValue = 'updated_value'; 87 | 88 | // Mock Storage to avoid file system issues 89 | Storage::shouldReceive('put') 90 | ->with(\Mockery::type('string'), \Mockery::type('string')) 91 | ->andReturn(true); 92 | 93 | // Set initial value 94 | $this->configUtil->setSetting($key, $initialValue); 95 | 96 | // Update the value 97 | $this->configUtil->setSetting($key, $updatedValue); 98 | 99 | // Test passes if no exception is thrown 100 | $this->assertTrue(true); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /tests/Unit/Utilities/FeatureToggleUtilTest.php: -------------------------------------------------------------------------------- 1 | assertTrue($result); 42 | } 43 | 44 | public function test_feature_is_disabled_when_configured_false() 45 | { 46 | Config::set('feature-toggles.test_feature', false); 47 | 48 | $result = FeatureToggleUtil::isEnabled('test_feature'); 49 | 50 | $this->assertFalse($result); 51 | } 52 | 53 | public function test_feature_is_disabled_by_default() 54 | { 55 | $result = FeatureToggleUtil::isEnabled('non_existent_feature'); 56 | 57 | $this->assertFalse($result); 58 | } 59 | 60 | public function test_user_override_takes_precedence() 61 | { 62 | // Set base feature to false 63 | Config::set('feature-toggles.test_feature', false); 64 | 65 | // Mock authenticated user 66 | $user = new \stdClass(); 67 | $user->id = 123; 68 | Auth::shouldReceive('user')->andReturn($user); 69 | 70 | // Set user override to true 71 | Config::set('feature-toggles.test_feature.user.123', true); 72 | 73 | $result = FeatureToggleUtil::isEnabled('test_feature'); 74 | 75 | $this->assertTrue($result); 76 | } 77 | 78 | public function test_environment_override_takes_precedence() 79 | { 80 | // Set base feature to true 81 | Config::set('feature-toggles.test_feature', true); 82 | 83 | // Mock no authenticated user 84 | Auth::shouldReceive('user')->andReturn(null); 85 | 86 | // Set environment override to false 87 | Config::set('feature-toggles.test_feature.environment.testing', false); 88 | 89 | $result = FeatureToggleUtil::isEnabled('test_feature'); 90 | 91 | $this->assertFalse($result); 92 | } 93 | 94 | public function test_creates_config_file_if_not_exists() 95 | { 96 | $configPath = config_path('feature-toggles.php'); 97 | 98 | // Ensure file doesn't exist 99 | if (File::exists($configPath)) { 100 | File::delete($configPath); 101 | } 102 | 103 | $this->assertFalse(File::exists($configPath)); 104 | 105 | // Call isEnabled which should create the config file 106 | FeatureToggleUtil::isEnabled('test_feature'); 107 | 108 | $this->assertTrue(File::exists($configPath)); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /tests/Unit/Utilities/FilteringUtilTest.php: -------------------------------------------------------------------------------- 1 | testCollection = collect([ 18 | ['name' => 'John Doe', 'age' => 25, 'email' => 'john@example.com'], 19 | ['name' => 'Jane Smith', 'age' => 30, 'email' => 'jane@example.com'], 20 | ['name' => 'Bob Wilson', 'age' => 35, 'email' => 'bob@test.com'], 21 | ['name' => 'Alice Brown', 'age' => 28, 'email' => 'alice@example.com'], 22 | ]); 23 | } 24 | 25 | public function test_can_filter_by_equals_operator() 26 | { 27 | $result = FilteringUtil::filter($this->testCollection, 'age', 'equals', 25); 28 | 29 | $this->assertCount(1, $result); 30 | $this->assertEquals('John Doe', $result->first()['name']); 31 | } 32 | 33 | public function test_can_filter_by_not_equals_operator() 34 | { 35 | $result = FilteringUtil::filter($this->testCollection, 'age', 'not_equals', 25); 36 | 37 | $this->assertCount(3, $result); 38 | $this->assertNotContains(['name' => 'John Doe'], $result->toArray()); 39 | } 40 | 41 | public function test_can_filter_by_contains_operator() 42 | { 43 | $result = FilteringUtil::filter($this->testCollection, 'email', 'contains', 'example'); 44 | 45 | $this->assertCount(3, $result); 46 | $this->assertNotContains(['name' => 'Bob Wilson'], $result->toArray()); 47 | } 48 | 49 | public function test_can_filter_by_not_contains_operator() 50 | { 51 | $result = FilteringUtil::filter($this->testCollection, 'email', 'not_contains', 'example'); 52 | 53 | $this->assertCount(1, $result); 54 | $this->assertEquals('Bob Wilson', $result->first()['name']); 55 | } 56 | 57 | public function test_can_filter_by_starts_with_operator() 58 | { 59 | $result = FilteringUtil::filter($this->testCollection, 'name', 'starts_with', 'John'); 60 | 61 | $this->assertCount(1, $result); 62 | $this->assertEquals('John Doe', $result->first()['name']); 63 | } 64 | 65 | public function test_can_filter_by_ends_with_operator() 66 | { 67 | $result = FilteringUtil::filter($this->testCollection, 'name', 'ends_with', 'Smith'); 68 | 69 | $this->assertCount(1, $result); 70 | $this->assertEquals('Jane Smith', $result->first()['name']); 71 | } 72 | 73 | public function test_returns_empty_collection_for_unknown_operator() 74 | { 75 | $result = FilteringUtil::filter($this->testCollection, 'name', 'unknown_operator', 'test'); 76 | 77 | $this->assertCount(0, $result); 78 | } 79 | 80 | public function test_filtering_is_case_insensitive() 81 | { 82 | $result = FilteringUtil::filter($this->testCollection, 'name', 'contains', 'john'); 83 | 84 | $this->assertCount(1, $result); 85 | $this->assertEquals('John Doe', $result->first()['name']); 86 | } 87 | 88 | public function test_can_filter_nested_data() 89 | { 90 | $nestedCollection = collect([ 91 | ['user' => ['name' => 'John', 'profile' => ['age' => 25]]], 92 | ['user' => ['name' => 'Jane', 'profile' => ['age' => 30]]], 93 | ]); 94 | 95 | $result = FilteringUtil::filter($nestedCollection, 'user.profile.age', 'equals', 25); 96 | 97 | $this->assertCount(1, $result); 98 | $this->assertEquals('John', $result->first()['user']['name']); 99 | } 100 | 101 | public function test_handles_missing_data_gracefully() 102 | { 103 | $result = FilteringUtil::filter($this->testCollection, 'non_existent_field', 'equals', 'value'); 104 | 105 | $this->assertCount(0, $result); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /tests/Unit/Utilities/LoggingUtilTest.php: -------------------------------------------------------------------------------- 1 | createMock(\Monolog\Logger::class); 28 | $mockLogger->method('info')->willReturnSelf(); 29 | $mockLogger->method('debug')->willReturnSelf(); 30 | $mockLogger->method('warning')->willReturnSelf(); 31 | $mockLogger->method('error')->willReturnSelf(); 32 | $mockLogger->method('critical')->willReturnSelf(); 33 | 34 | // Use reflection to set the custom logger 35 | $reflection = new \ReflectionClass(LoggingUtil::class); 36 | $property = $reflection->getProperty('customLogger'); 37 | $property->setAccessible(true); 38 | $property->setValue(null, $mockLogger); 39 | 40 | return $mockLogger; 41 | } 42 | 43 | protected function tearDown(): void 44 | { 45 | // Clean up log file after each test 46 | $logFile = storage_path('logs/custom.log'); 47 | if (file_exists($logFile)) { 48 | unlink($logFile); 49 | } 50 | 51 | parent::tearDown(); 52 | } 53 | 54 | public function test_can_log_with_info_level() 55 | { 56 | $message = 'Test info message'; 57 | $context = ['key' => 'value']; 58 | 59 | $this->mockLogger(); 60 | LoggingUtil::info($message, $context); 61 | 62 | // Test passes if no exception is thrown 63 | $this->assertTrue(true); 64 | } 65 | 66 | public function test_can_log_with_debug_level() 67 | { 68 | $message = 'Test debug message'; 69 | $context = ['debug_key' => 'debug_value']; 70 | 71 | $this->mockLogger(); 72 | LoggingUtil::debug($message, $context); 73 | 74 | // Test passes if no exception is thrown 75 | $this->assertTrue(true); 76 | } 77 | 78 | public function test_can_log_with_warning_level() 79 | { 80 | $message = 'Test warning message'; 81 | $context = ['warning_key' => 'warning_value']; 82 | 83 | $this->mockLogger(); 84 | LoggingUtil::warning($message, $context); 85 | 86 | // Test passes if no exception is thrown 87 | $this->assertTrue(true); 88 | } 89 | 90 | public function test_can_log_with_error_level() 91 | { 92 | $message = 'Test error message'; 93 | $context = ['error_key' => 'error_value']; 94 | 95 | $this->mockLogger(); 96 | LoggingUtil::error($message, $context); 97 | 98 | // Test passes if no exception is thrown 99 | $this->assertTrue(true); 100 | } 101 | 102 | public function test_can_log_with_critical_level() 103 | { 104 | $message = 'Test critical message'; 105 | $context = ['critical_key' => 'critical_value']; 106 | 107 | $this->mockLogger(); 108 | LoggingUtil::critical($message, $context); 109 | 110 | // Test passes if no exception is thrown 111 | $this->assertTrue(true); 112 | } 113 | 114 | public function test_can_log_with_custom_channel() 115 | { 116 | $message = 'Test channel message'; 117 | $context = ['channel_key' => 'channel_value']; 118 | $channel = 'custom_channel'; 119 | 120 | // Mock the Log facade to verify channel is used 121 | $mockLogger = $this->createMock(\Monolog\Logger::class); 122 | $mockLogger->method('info')->willReturnSelf(); 123 | 124 | Log::shouldReceive('channel') 125 | ->with($channel) 126 | ->once() 127 | ->andReturn($mockLogger); 128 | 129 | LoggingUtil::info($message, $context, $channel); 130 | 131 | // Test passes if no exception is thrown 132 | $this->assertTrue(true); 133 | } 134 | 135 | public function test_includes_timestamp_in_context() 136 | { 137 | $message = 'Test timestamp message'; 138 | $context = ['test_key' => 'test_value']; 139 | 140 | // Mock the logger to avoid file system issues 141 | $mockLogger = $this->createMock(\Monolog\Logger::class); 142 | $mockLogger->method('info')->willReturnSelf(); 143 | 144 | // Use reflection to set the custom logger 145 | $reflection = new \ReflectionClass(LoggingUtil::class); 146 | $property = $reflection->getProperty('customLogger'); 147 | $property->setAccessible(true); 148 | $property->setValue(null, $mockLogger); 149 | 150 | LoggingUtil::info($message, $context); 151 | 152 | // Test passes if no exception is thrown 153 | $this->assertTrue(true); 154 | } 155 | 156 | public function test_includes_environment_in_context() 157 | { 158 | Config::set('app.env', 'testing'); 159 | 160 | $message = 'Test environment message'; 161 | $context = ['test_key' => 'test_value']; 162 | 163 | $this->mockLogger(); 164 | LoggingUtil::info($message, $context); 165 | 166 | // Test passes if no exception is thrown 167 | $this->assertTrue(true); 168 | } 169 | 170 | public function test_can_use_generic_log_method() 171 | { 172 | $message = 'Test generic log message'; 173 | $context = ['test_key' => 'test_value']; 174 | 175 | $this->mockLogger(); 176 | LoggingUtil::log(LogLevel::Info, $message, $context); 177 | 178 | // Test passes if no exception is thrown 179 | $this->assertTrue(true); 180 | } 181 | 182 | public function test_logs_are_in_json_format() 183 | { 184 | $message = 'Test JSON format message'; 185 | $context = ['json_key' => 'json_value']; 186 | 187 | $this->mockLogger(); 188 | LoggingUtil::info($message, $context); 189 | 190 | // Test passes if no exception is thrown 191 | $this->assertTrue(true); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /tests/Unit/Utilities/PaginationUtilTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(LengthAwarePaginator::class, $paginator); 23 | $this->assertEquals(25, $paginator->total()); 24 | $this->assertEquals(10, $paginator->perPage()); 25 | $this->assertEquals(1, $paginator->currentPage()); 26 | $this->assertCount(10, $paginator->items()); 27 | $this->assertEquals([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], $paginator->items()); 28 | } 29 | 30 | public function test_can_paginate_array_second_page() 31 | { 32 | $items = range(1, 25); // 25 items 33 | $perPage = 10; 34 | $currentPage = 2; 35 | 36 | $paginator = PaginationUtil::paginate($items, $perPage, $currentPage); 37 | 38 | $this->assertEquals(2, $paginator->currentPage()); 39 | $this->assertCount(10, $paginator->items()); 40 | $this->assertEquals([11, 12, 13, 14, 15, 16, 17, 18, 19, 20], $paginator->items()); 41 | } 42 | 43 | public function test_can_paginate_array_last_page() 44 | { 45 | $items = range(1, 25); // 25 items 46 | $perPage = 10; 47 | $currentPage = 3; 48 | 49 | $paginator = PaginationUtil::paginate($items, $perPage, $currentPage); 50 | 51 | $this->assertEquals(3, $paginator->currentPage()); 52 | $this->assertCount(5, $paginator->items()); 53 | $this->assertEquals([21, 22, 23, 24, 25], $paginator->items()); 54 | } 55 | 56 | public function test_can_paginate_array_with_options() 57 | { 58 | $items = range(1, 10); 59 | $perPage = 5; 60 | $currentPage = 1; 61 | $options = ['path' => '/test', 'pageName' => 'p']; 62 | 63 | $paginator = PaginationUtil::paginate($items, $perPage, $currentPage, $options); 64 | 65 | $this->assertInstanceOf(LengthAwarePaginator::class, $paginator); 66 | $this->assertEquals(10, $paginator->total()); 67 | } 68 | 69 | public function test_handles_empty_array() 70 | { 71 | $items = []; 72 | $perPage = 10; 73 | $currentPage = 1; 74 | 75 | $paginator = PaginationUtil::paginate($items, $perPage, $currentPage); 76 | 77 | $this->assertEquals(0, $paginator->total()); 78 | $this->assertCount(0, $paginator->items()); 79 | } 80 | 81 | public function test_handles_page_beyond_available_data() 82 | { 83 | $items = range(1, 5); 84 | $perPage = 10; 85 | $currentPage = 2; 86 | 87 | $paginator = PaginationUtil::paginate($items, $perPage, $currentPage); 88 | 89 | $this->assertEquals(5, $paginator->total()); 90 | $this->assertCount(0, $paginator->items()); 91 | } 92 | 93 | public function test_can_paginate_query_builder() 94 | { 95 | // Create a mock query builder 96 | $query = $this->createMock(Builder::class); 97 | $query->method('paginate')->willReturn(new LengthAwarePaginator( 98 | [1, 2, 3, 4, 5], 99 | 10, 100 | 2, 101 | 1, 102 | ['path' => '/test'] 103 | )); 104 | 105 | $perPage = 2; 106 | $page = 1; 107 | $options = ['path' => '/test']; 108 | 109 | $paginator = PaginationUtil::paginateQuery($query, $perPage, $page, $options); 110 | 111 | $this->assertInstanceOf(LengthAwarePaginator::class, $paginator); 112 | } 113 | 114 | public function test_paginate_query_uses_request_page_when_no_page_provided() 115 | { 116 | // Mock the request 117 | $request = Request::create('/test', 'GET', ['page' => 3]); 118 | $this->app->instance('request', $request); 119 | 120 | $query = $this->createMock(Builder::class); 121 | $query->method('paginate')->willReturn(new LengthAwarePaginator( 122 | [1, 2, 3, 4, 5], 123 | 10, 124 | 2, 125 | 3, 126 | ['path' => '/test'] 127 | )); 128 | 129 | $perPage = 2; 130 | $options = ['path' => '/test']; 131 | 132 | $paginator = PaginationUtil::paginateQuery($query, $perPage, null, $options); 133 | 134 | $this->assertInstanceOf(LengthAwarePaginator::class, $paginator); 135 | } 136 | 137 | public function test_paginate_query_defaults_to_page_1_when_no_request_page() 138 | { 139 | // Mock the request with no page parameter 140 | $request = Request::create('/test', 'GET', []); 141 | $this->app->instance('request', $request); 142 | 143 | $query = $this->createMock(Builder::class); 144 | $query->method('paginate')->willReturn(new LengthAwarePaginator( 145 | [1, 2, 3, 4, 5], 146 | 10, 147 | 2, 148 | 1, 149 | ['path' => '/test'] 150 | )); 151 | 152 | $perPage = 2; 153 | $options = ['path' => '/test']; 154 | 155 | $paginator = PaginationUtil::paginateQuery($query, $perPage, null, $options); 156 | 157 | $this->assertInstanceOf(LengthAwarePaginator::class, $paginator); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /tests/Unit/Utilities/QueryParameterUtilTest.php: -------------------------------------------------------------------------------- 1 | 'John Doe', 15 | 'email' => 'john@example.com', 16 | 'age' => 25, 17 | 'unwanted_param' => 'should_be_ignored' 18 | ]); 19 | 20 | $allowedParameters = ['name', 'email', 'age']; 21 | 22 | $result = QueryParameterUtil::parse($request, $allowedParameters); 23 | 24 | $this->assertArrayHasKey('name', $result); 25 | $this->assertArrayHasKey('email', $result); 26 | $this->assertArrayHasKey('age', $result); 27 | $this->assertArrayNotHasKey('unwanted_param', $result); 28 | 29 | $this->assertEquals('John Doe', $result['name']); 30 | $this->assertEquals('john@example.com', $result['email']); 31 | $this->assertEquals(25, $result['age']); 32 | } 33 | 34 | public function test_ignores_unallowed_parameters() 35 | { 36 | $request = Request::create('/test', 'GET', [ 37 | 'allowed_param' => 'allowed_value', 38 | 'forbidden_param' => 'forbidden_value', 39 | 'another_forbidden' => 'another_value' 40 | ]); 41 | 42 | $allowedParameters = ['allowed_param']; 43 | 44 | $result = QueryParameterUtil::parse($request, $allowedParameters); 45 | 46 | $this->assertCount(1, $result); 47 | $this->assertArrayHasKey('allowed_param', $result); 48 | $this->assertArrayNotHasKey('forbidden_param', $result); 49 | $this->assertArrayNotHasKey('another_forbidden', $result); 50 | } 51 | 52 | public function test_handles_empty_allowed_parameters() 53 | { 54 | $request = Request::create('/test', 'GET', [ 55 | 'param1' => 'value1', 56 | 'param2' => 'value2' 57 | ]); 58 | 59 | $allowedParameters = []; 60 | 61 | $result = QueryParameterUtil::parse($request, $allowedParameters); 62 | 63 | $this->assertEmpty($result); 64 | } 65 | 66 | public function test_handles_missing_parameters() 67 | { 68 | $request = Request::create('/test', 'GET', []); 69 | 70 | $allowedParameters = ['name', 'email', 'age']; 71 | 72 | $result = QueryParameterUtil::parse($request, $allowedParameters); 73 | 74 | $this->assertEmpty($result); 75 | } 76 | 77 | public function test_handles_partial_parameters() 78 | { 79 | $request = Request::create('/test', 'GET', [ 80 | 'name' => 'John Doe', 81 | 'unwanted' => 'ignored' 82 | ]); 83 | 84 | $allowedParameters = ['name', 'email', 'age']; 85 | 86 | $result = QueryParameterUtil::parse($request, $allowedParameters); 87 | 88 | $this->assertCount(1, $result); 89 | $this->assertArrayHasKey('name', $result); 90 | $this->assertEquals('John Doe', $result['name']); 91 | } 92 | 93 | public function test_preserves_parameter_types() 94 | { 95 | $request = Request::create('/test', 'GET', [ 96 | 'string_param' => 'string_value', 97 | 'int_param' => '123', 98 | 'bool_param' => 'true', 99 | 'array_param' => ['item1', 'item2'] 100 | ]); 101 | 102 | $allowedParameters = ['string_param', 'int_param', 'bool_param', 'array_param']; 103 | 104 | $result = QueryParameterUtil::parse($request, $allowedParameters); 105 | 106 | $this->assertIsString($result['string_param']); 107 | $this->assertIsString($result['int_param']); // Request parameters are always strings 108 | $this->assertIsString($result['bool_param']); // Request parameters are always strings 109 | $this->assertIsArray($result['array_param']); 110 | } 111 | 112 | public function test_handles_duplicate_parameter_names() 113 | { 114 | $request = Request::create('/test', 'GET', [ 115 | 'param' => 'value1' 116 | ]); 117 | 118 | $allowedParameters = ['param', 'param']; // Duplicate in allowed list 119 | 120 | $result = QueryParameterUtil::parse($request, $allowedParameters); 121 | 122 | $this->assertCount(1, $result); 123 | $this->assertArrayHasKey('param', $result); 124 | $this->assertEquals('value1', $result['param']); 125 | } 126 | 127 | public function test_handles_empty_string_parameters() 128 | { 129 | $request = Request::create('/test', 'GET', [ 130 | 'empty_param' => '', 131 | 'normal_param' => 'normal_value' 132 | ]); 133 | 134 | $allowedParameters = ['empty_param', 'normal_param']; 135 | 136 | $result = QueryParameterUtil::parse($request, $allowedParameters); 137 | 138 | $this->assertCount(2, $result); 139 | $this->assertEquals('', $result['empty_param']); 140 | $this->assertEquals('normal_value', $result['normal_param']); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /tests/Unit/Utilities/RateLimiterUtilTest.php: -------------------------------------------------------------------------------- 1 | cache = new \Illuminate\Cache\Repository(new ArrayStore()); 21 | $this->rateLimiterUtil = new RateLimiterUtil($this->cache); 22 | } 23 | 24 | public function test_can_attempt_within_limit() 25 | { 26 | $key = 'test_key'; 27 | $maxAttempts = 5; 28 | $decayMinutes = 1; 29 | 30 | $result = $this->rateLimiterUtil->attempt($key, $maxAttempts, $decayMinutes); 31 | 32 | $this->assertTrue($result); 33 | } 34 | 35 | public function test_blocks_after_exceeding_limit() 36 | { 37 | $key = 'test_key_limit'; 38 | $maxAttempts = 2; 39 | $decayMinutes = 1; 40 | 41 | // First attempt should succeed 42 | $result1 = $this->rateLimiterUtil->attempt($key, $maxAttempts, $decayMinutes); 43 | $this->assertTrue($result1); 44 | 45 | // Second attempt should succeed 46 | $result2 = $this->rateLimiterUtil->attempt($key, $maxAttempts, $decayMinutes); 47 | $this->assertTrue($result2); 48 | 49 | // Third attempt should fail 50 | $result3 = $this->rateLimiterUtil->attempt($key, $maxAttempts, $decayMinutes); 51 | $this->assertFalse($result3); 52 | } 53 | 54 | public function test_can_get_attempts_count() 55 | { 56 | $key = 'test_key_attempts'; 57 | $maxAttempts = 5; 58 | $decayMinutes = 1; 59 | 60 | // No attempts initially 61 | $this->assertEquals(0, $this->rateLimiterUtil->attempts($key)); 62 | 63 | // After one attempt 64 | $this->rateLimiterUtil->attempt($key, $maxAttempts, $decayMinutes); 65 | $this->assertEquals(1, $this->rateLimiterUtil->attempts($key)); 66 | 67 | // After two attempts 68 | $this->rateLimiterUtil->attempt($key, $maxAttempts, $decayMinutes); 69 | $this->assertEquals(2, $this->rateLimiterUtil->attempts($key)); 70 | } 71 | 72 | public function test_can_get_remaining_attempts() 73 | { 74 | $key = 'test_key_remaining'; 75 | $maxAttempts = 3; 76 | $decayMinutes = 1; 77 | 78 | // Initially should have max attempts 79 | $this->assertEquals(3, $this->rateLimiterUtil->remaining($key, $maxAttempts)); 80 | 81 | // After one attempt 82 | $this->rateLimiterUtil->attempt($key, $maxAttempts, $decayMinutes); 83 | $this->assertEquals(2, $this->rateLimiterUtil->remaining($key, $maxAttempts)); 84 | 85 | // After two attempts 86 | $this->rateLimiterUtil->attempt($key, $maxAttempts, $decayMinutes); 87 | $this->assertEquals(1, $this->rateLimiterUtil->remaining($key, $maxAttempts)); 88 | 89 | // After three attempts 90 | $this->rateLimiterUtil->attempt($key, $maxAttempts, $decayMinutes); 91 | $this->assertEquals(0, $this->rateLimiterUtil->remaining($key, $maxAttempts)); 92 | } 93 | 94 | public function test_can_clear_attempts() 95 | { 96 | $key = 'test_key_clear'; 97 | $maxAttempts = 2; 98 | $decayMinutes = 1; 99 | 100 | // Make some attempts 101 | $this->rateLimiterUtil->attempt($key, $maxAttempts, $decayMinutes); 102 | $this->rateLimiterUtil->attempt($key, $maxAttempts, $decayMinutes); 103 | 104 | $this->assertEquals(2, $this->rateLimiterUtil->attempts($key)); 105 | 106 | // Clear the attempts 107 | $this->rateLimiterUtil->clear($key); 108 | 109 | $this->assertEquals(0, $this->rateLimiterUtil->attempts($key)); 110 | } 111 | 112 | public function test_can_check_if_too_many_attempts() 113 | { 114 | $key = 'test_key_too_many'; 115 | $maxAttempts = 2; 116 | $decayMinutes = 1; 117 | 118 | // Initially not too many attempts 119 | $this->assertFalse($this->rateLimiterUtil->tooManyAttempts($key, $maxAttempts)); 120 | 121 | // After exceeding limit 122 | $this->rateLimiterUtil->attempt($key, $maxAttempts, $decayMinutes); 123 | $this->rateLimiterUtil->attempt($key, $maxAttempts, $decayMinutes); 124 | $this->rateLimiterUtil->attempt($key, $maxAttempts, $decayMinutes); 125 | 126 | $this->assertTrue($this->rateLimiterUtil->tooManyAttempts($key, $maxAttempts)); 127 | } 128 | 129 | public function test_can_hit_rate_limiter() 130 | { 131 | $key = 'test_key_hit'; 132 | $decaySeconds = 60; 133 | 134 | $result = $this->rateLimiterUtil->hit($key, $decaySeconds); 135 | 136 | $this->assertIsInt($result); 137 | $this->assertEquals(1, $this->rateLimiterUtil->attempts($key)); 138 | } 139 | 140 | public function test_can_get_available_in_time() 141 | { 142 | $key = 'test_key_available'; 143 | $maxAttempts = 1; 144 | $decayMinutes = 1; 145 | 146 | // Initially available 147 | $this->assertEquals(0, $this->rateLimiterUtil->availableIn($key)); 148 | 149 | // After hitting limit 150 | $this->rateLimiterUtil->attempt($key, $maxAttempts, $decayMinutes); 151 | $this->rateLimiterUtil->attempt($key, $maxAttempts, $decayMinutes); 152 | 153 | $availableIn = $this->rateLimiterUtil->availableIn($key); 154 | $this->assertGreaterThan(0, $availableIn); 155 | $this->assertLessThanOrEqual(60, $availableIn); // Should be within decay time 156 | } 157 | 158 | public function test_can_get_rate_limiter_instance() 159 | { 160 | $rateLimiter = $this->rateLimiterUtil->getRateLimiter(); 161 | 162 | $this->assertInstanceOf(RateLimiter::class, $rateLimiter); 163 | } 164 | 165 | public function test_different_keys_are_independent() 166 | { 167 | $key1 = 'test_key_1'; 168 | $key2 = 'test_key_2'; 169 | $maxAttempts = 1; 170 | $decayMinutes = 1; 171 | 172 | // Hit limit for key1 173 | $this->rateLimiterUtil->attempt($key1, $maxAttempts, $decayMinutes); 174 | $this->rateLimiterUtil->attempt($key1, $maxAttempts, $decayMinutes); 175 | 176 | // key2 should still be available 177 | $this->assertTrue($this->rateLimiterUtil->attempt($key2, $maxAttempts, $decayMinutes)); 178 | 179 | // key1 should be blocked 180 | $this->assertFalse($this->rateLimiterUtil->attempt($key1, $maxAttempts, $decayMinutes)); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /tests/Unit/Utilities/SchedulerUtilTest.php: -------------------------------------------------------------------------------- 1 | schedulerUtil = new SchedulerUtil(); 18 | } 19 | 20 | public function test_can_get_schedule_summary() 21 | { 22 | // Mock empty Schedule 23 | $schedule = $this->createMock(Schedule::class); 24 | $schedule->method('events')->willReturn([]); 25 | 26 | $this->app->instance(Schedule::class, $schedule); 27 | 28 | $summary = $this->schedulerUtil->getScheduleSummary(); 29 | 30 | $this->assertIsArray($summary); 31 | $this->assertEmpty($summary); 32 | } 33 | 34 | public function test_can_check_for_overdue_tasks() 35 | { 36 | // Mock empty Schedule 37 | $schedule = $this->createMock(Schedule::class); 38 | $schedule->method('events')->willReturn([]); 39 | 40 | $this->app->instance(Schedule::class, $schedule); 41 | 42 | $hasOverdue = $this->schedulerUtil->hasOverdueTasks(); 43 | 44 | $this->assertIsBool($hasOverdue); 45 | } 46 | 47 | public function test_returns_false_when_no_overdue_tasks() 48 | { 49 | // Mock empty Schedule 50 | $schedule = $this->createMock(Schedule::class); 51 | $schedule->method('events')->willReturn([]); 52 | 53 | $this->app->instance(Schedule::class, $schedule); 54 | 55 | $hasOverdue = $this->schedulerUtil->hasOverdueTasks(); 56 | 57 | $this->assertFalse($hasOverdue); 58 | } 59 | 60 | public function test_ignores_running_tasks_for_overdue_check() 61 | { 62 | // Mock empty Schedule 63 | $schedule = $this->createMock(Schedule::class); 64 | $schedule->method('events')->willReturn([]); 65 | 66 | $this->app->instance(Schedule::class, $schedule); 67 | 68 | $hasOverdue = $this->schedulerUtil->hasOverdueTasks(); 69 | 70 | $this->assertFalse($hasOverdue); 71 | } 72 | 73 | public function test_handles_empty_schedule() 74 | { 75 | // Mock empty Schedule 76 | $schedule = $this->createMock(Schedule::class); 77 | $schedule->method('events')->willReturn([]); 78 | 79 | $this->app->instance(Schedule::class, $schedule); 80 | 81 | $summary = $this->schedulerUtil->getScheduleSummary(); 82 | $hasOverdue = $this->schedulerUtil->hasOverdueTasks(); 83 | 84 | $this->assertIsArray($summary); 85 | $this->assertEmpty($summary); 86 | $this->assertFalse($hasOverdue); 87 | } 88 | 89 | public function test_logs_scheduled_events() 90 | { 91 | // Mock empty Schedule 92 | $schedule = $this->createMock(Schedule::class); 93 | $schedule->method('events')->willReturn([]); 94 | 95 | $this->app->instance(Schedule::class, $schedule); 96 | 97 | // Mock Log facade to verify logging 98 | Log::shouldReceive('info') 99 | ->with(\Mockery::type('string')) 100 | ->once(); 101 | 102 | $this->schedulerUtil->getScheduleSummary(); 103 | } 104 | 105 | public function test_handles_multiple_events() 106 | { 107 | // Mock empty Schedule 108 | $schedule = $this->createMock(Schedule::class); 109 | $schedule->method('events')->willReturn([]); 110 | 111 | $this->app->instance(Schedule::class, $schedule); 112 | 113 | $summary = $this->schedulerUtil->getScheduleSummary(); 114 | 115 | $this->assertIsArray($summary); 116 | $this->assertEmpty($summary); 117 | } 118 | } --------------------------------------------------------------------------------