├── src ├── Models │ └── Plan.php ├── Facades │ └── PlanCraft.php ├── PlanCraftServiceProvider.php ├── Plan.php ├── Commands │ └── InstallCommand.php ├── PlanCraft.php └── Trait │ └── HasPlan.php ├── database ├── factories │ └── PlanFactory.php └── migrations │ └── 2023_08_12_000000_create_plans_table.php ├── LICENSE.md ├── stubs └── app │ └── Providers │ └── PlanCraftServiceProvider.stub ├── CONTRIBUTING.md ├── CHANGELOG.md ├── composer.json └── README.md /src/Models/Plan.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | public function definition(): array 15 | { 16 | return [ 17 | 'key' => $this->faker->word(), 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /database/migrations/2023_08_12_000000_create_plans_table.php: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->unsignedBigInteger('planable_id'); 14 | $table->string('planable_type'); 15 | $table->string('key')->nullable(); 16 | $table->timestamps(); 17 | }); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) realrashid 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/PlanCraftServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->runningInConsole()) { 26 | $this->configurePublishing(); 27 | $this->configureCommands(); 28 | $this->loadMigrationsFrom(__DIR__.'/../database/migrations/2023_08_12_000000_create_plans_table.php'); 29 | } 30 | } 31 | 32 | /** 33 | * Configure publishing for the package. 34 | */ 35 | public function configurePublishing() 36 | { 37 | $this->publishes([ 38 | __DIR__.'/../stubs/app/Providers/PlanCraftServiceProvider.stub' => app_path('Providers/PlanCraftServiceProvider.php'), 39 | ], 'plancraft-provider'); 40 | 41 | } 42 | 43 | /** 44 | * Configure the commands. 45 | */ 46 | public function configureCommands() 47 | { 48 | $this->commands([ 49 | InstallCommand::class, 50 | ]); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /stubs/app/Providers/PlanCraftServiceProvider.stub: -------------------------------------------------------------------------------- 1 | definePlans(); 24 | } 25 | 26 | protected function definePlans(): void 27 | { 28 | PlanCraft::create('basic', 'Basic', '10|120', 'Monthly|Yearly', [ 29 | '10 Chirps', 30 | '5 Teams', 31 | ], [ 32 | 'max_chirps' => 10, 33 | 'max_teams' => 5, 34 | ], 'price_monthly|price_yearly')->description('Basic plan users can create 10 Chirps on per team and Create 5 Teams.'); 35 | 36 | PlanCraft::create('pro', 'Pro', '19|228', 'Monthly|Yearly', [ 37 | '20 Chirps', 38 | '30 Teams', 39 | ], [ 40 | 'max_chirps' => 20, 41 | 'max_teams' => 30, 42 | ], 'price_monthly|price_yearly')->description('Pro plan users can create 20 Chirps on per team and Create 30 Teams.'); 43 | 44 | PlanCraft::create('unlimited', 'Unlimited', '50|600', 'Monthly|Yearly', [ 45 | 'Unlimited Chirps', 46 | 'Unlimited Teams', 47 | ], [ 48 | 'max_chirps' => 0, 49 | 'max_teams' => 0, 50 | ], 'price_monthly|price_yearly')->description('Unlimited plan users can create unlimited.'); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | Please read and understand the contribution guide before creating an issue or pull request. 6 | 7 | ## Etiquette 8 | 9 | This project is open source, and as such, I dedicate my free time to build and maintain the source code. It is made available in the hope that it will be useful to other developers. It would be unfair to suffer abuse or anger for my hard work. 10 | 11 | Please be considerate when raising issues or presenting pull requests. Let's show that developers are respectful and collaborative. 12 | 13 | It is my duty to ensure that all submissions are of sufficient quality to benefit the project. Respect my decisions, and please do not be upset if your submission is not used. 14 | 15 | ## Viability 16 | 17 | Before requesting or submitting new features, consider whether it will be useful to others. Open source projects are used by many developers with different needs. Think about whether your feature might be beneficial to other users of the project. 18 | 19 | ## Procedure 20 | 21 | Before filing an issue: 22 | 23 | - Attempt to replicate the problem to ensure it wasn't a coincidental incident. 24 | - Check if your feature suggestion is already present in the project. 25 | - Look through the pull requests tab to ensure the bug doesn’t already have a fix in progress. 26 | - Check the pull requests tab to see if the feature is already being worked on. 27 | 28 | Before submitting a pull request: 29 | 30 | - Verify that your feature doesn’t already exist in the codebase. 31 | - Check the pull requests to ensure that someone else hasn’t already submitted the feature or fix. 32 | 33 | ## Requirements 34 | 35 | If there are additional requirements for the project, you will find them listed here: 36 | 37 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)**: Install [PHP Code Sniffer](https://pear.php.net/package/PHP_CodeSniffer) to apply the conventions easily. 38 | 39 | - **Add tests**: Your patch won’t be accepted if it doesn’t include tests. 40 | 41 | - **Document changes**: Update the `README.md` and any other relevant documentation to reflect changes in behavior. 42 | 43 | - **Respect our release cycle**: Follow [SemVer v2.0.0](https://semver.org/) and avoid randomly breaking public APIs. 44 | 45 | - **One pull request per feature**: If you want to implement multiple features, please submit separate pull requests for each. 46 | 47 | - **Send coherent history**: Ensure each commit in your pull request is meaningful. If you made multiple intermediate commits, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submission. 48 | 49 | **Happy coding**! 50 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `PlanCraft` will be documented in this file. 4 | 5 | ## v1.1.2 - 2025-12-02 6 | 7 | ### What's Changed 8 | 9 | * Bump dependabot/fetch-metadata from 2.1.0 to 2.2.0 by @dependabot[bot] in https://github.com/realrashid/plan-craft/pull/12 10 | * Update installation.md by @wagood in https://github.com/realrashid/plan-craft/pull/14 11 | * fix 'bisic' typo to 'basic' by @harrisonratcliffe in https://github.com/realrashid/plan-craft/pull/16 12 | * Update usage.md by @wagood in https://github.com/realrashid/plan-craft/pull/15 13 | * Bump dependabot/fetch-metadata from 2.2.0 to 2.3.0 by @dependabot[bot] in https://github.com/realrashid/plan-craft/pull/17 14 | * Bump aglipanci/laravel-pint-action from 2.4 to 2.5 by @dependabot[bot] in https://github.com/realrashid/plan-craft/pull/18 15 | * Bump dependabot/fetch-metadata from 2.3.0 to 2.4.0 by @dependabot[bot] in https://github.com/realrashid/plan-craft/pull/20 16 | * Bump aglipanci/laravel-pint-action from 2.5 to 2.6 by @dependabot[bot] in https://github.com/realrashid/plan-craft/pull/24 17 | * Update composer.json for laravel 12 support by @webblufter in https://github.com/realrashid/plan-craft/pull/23 18 | * Bump actions/setup-node from 4 to 6 by @dependabot[bot] in https://github.com/realrashid/plan-craft/pull/29 19 | * Bump stefanzweifel/git-auto-commit-action from 5 to 7 by @dependabot[bot] in https://github.com/realrashid/plan-craft/pull/28 20 | * Bump actions/upload-pages-artifact from 3 to 4 by @dependabot[bot] in https://github.com/realrashid/plan-craft/pull/26 21 | 22 | ### New Contributors 23 | 24 | * @wagood made their first contribution in https://github.com/realrashid/plan-craft/pull/14 25 | * @harrisonratcliffe made their first contribution in https://github.com/realrashid/plan-craft/pull/16 26 | * @webblufter made their first contribution in https://github.com/realrashid/plan-craft/pull/23 27 | 28 | **Full Changelog**: https://github.com/realrashid/plan-craft/compare/v1.1.1...v1.1.2 29 | 30 | ## v1.1.1 - 2024-05-24 31 | 32 | ### What's Changed 33 | 34 | * Bump aglipanci/laravel-pint-action from 2.3.1 to 2.4 by @dependabot in https://github.com/realrashid/plan-craft/pull/9 35 | * Bump dependabot/fetch-metadata from 2.0.0 to 2.1.0 by @dependabot in https://github.com/realrashid/plan-craft/pull/10 36 | 37 | **Full Changelog**: https://github.com/realrashid/plan-craft/compare/V.1.1.0...v1.1.1 38 | 39 | ## V1.1.0 - 2024-04-11 40 | 41 | Added Laravel 11 Support. 42 | 43 | ## v1.0.0 - 2023-10-23 44 | 45 | ### [Initial Release] - 2023-10-23 46 | 47 | #### Added 48 | 49 | - Initial release of PlanCraft. 50 | - Features that simplify plan creation, management, and assignment. 51 | - Streamlined subscription plan management. 52 | - Support for advanced eligibility checks. 53 | - Innovative database-less plan management. 54 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "realrashid/plan-craft", 3 | "description": "Transforming Laravel Plans Management. Break free from conventional database storage constraints.", 4 | "keywords": [ 5 | "realrashid", 6 | "Laravel", 7 | "Plan-craft", 8 | "Plans", 9 | "Management", 10 | "Subscriptions", 11 | "SaaS" 12 | ], 13 | "homepage": "https://github.com/realrashid/plan-craft", 14 | "license": "MIT", 15 | "authors": [ 16 | { 17 | "name": "Rashid Ali", 18 | "email": "realrashid05@gmail.com", 19 | "role": "Developer" 20 | } 21 | ], 22 | "support": { 23 | "email": "realrashid05@gmail.com", 24 | "issues": "https://github.com/realrashid/plan-craft/issues", 25 | "source": "https://github.com/realrashid/plan-craft", 26 | "docs": "https://realrashid.github.io/plan-craft/" 27 | }, 28 | "require": { 29 | "php": "^8.1", 30 | "laravel/framework": "^8.0|^9.0|^10.0|^11.0|^12.0" 31 | }, 32 | "require-dev": { 33 | "laravel/pint": "^1.0", 34 | "mockery/mockery": "^1.6", 35 | "nunomaduro/collision": "^8.1.1||^7.10.0", 36 | "orchestra/testbench": "^6.0.0||^7.0.0||^8.20||^9.0.0", 37 | "pestphp/pest": "^2.34", 38 | "pestphp/pest-plugin-arch": "^2.7", 39 | "pestphp/pest-plugin-laravel": "^2.3" 40 | }, 41 | "autoload": { 42 | "psr-4": { 43 | "RealRashid\\PlanCraft\\": "src/", 44 | "RealRashid\\PlanCraft\\Database\\Factories\\": "database/factories/" 45 | } 46 | }, 47 | "autoload-dev": { 48 | "psr-4": { 49 | "App\\": "tests/Support/app/", 50 | "Database\\Factories\\": "tests/Support/database/factories/", 51 | "RealRashid\\PlanCraft\\Tests\\": "tests/" 52 | } 53 | }, 54 | "scripts": { 55 | "post-autoload-dump": "@composer run prepare", 56 | "clear": "@php vendor/bin/testbench package:purge-plancraft --ansi", 57 | "prepare": "@php vendor/bin/testbench package:discover --ansi", 58 | "build": [ 59 | "@composer run prepare", 60 | "@php vendor/bin/testbench workbench:build --ansi" 61 | ], 62 | "start": [ 63 | "Composer\\Config::disableProcessTimeout", 64 | "@composer run build", 65 | "@php vendor/bin/testbench serve" 66 | ], 67 | "analyse": "vendor/bin/phpstan analyse", 68 | "test": "vendor/bin/pest", 69 | "test-coverage": "vendor/bin/pest --coverage", 70 | "format": "vendor/bin/pint" 71 | }, 72 | "config": { 73 | "sort-packages": true, 74 | "allow-plugins": { 75 | "pestphp/pest-plugin": true, 76 | "phpstan/extension-installer": true 77 | } 78 | }, 79 | "extra": { 80 | "laravel": { 81 | "providers": [ 82 | "RealRashid\\PlanCraft\\PlanCraftServiceProvider" 83 | ], 84 | "aliases": { 85 | "PlanCraft": "RealRashid\\PlanCraft\\Facades\\PlanCraft" 86 | } 87 | } 88 | }, 89 | "minimum-stability": "dev", 90 | "prefer-stable": true 91 | } 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![PlanCraft](https://realrashid.github.io/plan-craft/assets/hero.63bbea69.webp) 2 | 3 | PlanCraft is a powerful Laravel package engineered to revolutionize how you manage subscription plans and features within your Laravel SaaS application. Say goodbye to the hassle of database wrangling and tedious queries. PlanCraft is your shortcut to seamless plan creation in Laravel. No more tables, no more queries - just pure, unadulterated efficiency. 4 | 5 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/realrashid/plan-craft.svg?style=flat-square)](https://packagist.org/packages/realrashid/plan-craft) 6 | [![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/realrashid/plan-craft/run-tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/realrashid/plan-craft/actions?query=workflow%3Arun-tests+branch%3Amain) 7 | [![GitHub Code Style Action Status](https://img.shields.io/github/actions/workflow/status/realrashid/plan-craft/fix-php-code-style-issues.yml?branch=main&label=code%20style&style=flat-square)](https://github.com/realrashid/plan-craft/actions?query=workflow%3A"Fix+PHP+code+style+issues"+branch%3Amain) 8 | [![Total Downloads](https://img.shields.io/packagist/dt/realrashid/plan-craft.svg?style=flat-square)](https://packagist.org/packages/realrashid/plan-craft) 9 | 10 | ## Documentation 11 | 12 | Ready to explore all the features and options PlanCraft offers? Dive into our comprehensive documentation: 13 | 14 | - [Installation Guide](https://realrashid.github.io/plan-craft/guide/installation) 15 | - [Usage Instructions](https://realrashid.github.io/plan-craft/usage/usage) 16 | - [Demo App](https://realrashid.github.io/plan-craft/demo/demo) 17 | 18 | ## Testing 19 | 20 | ```bash 21 | composer test 22 | ``` 23 | 24 | ## Changelog 25 | 26 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 27 | 28 | ## Contributing 29 | 30 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 31 | 32 | ## Security Vulnerabilities 33 | 34 | If you discover a security vulnerability in PlanCraft, please help us maintain the security of this project by responsibly disclosing it to us. To report a security vulnerability, please send an email to [realrashid@gmail.com](mailto:realrashid@gmail.com). We'll address the issue as promptly as possible. 35 | 36 | ## Credits 37 | 38 | - [Rashid Ali](https://github.com/realrashid) 39 | - [All Contributors](../../contributors) 40 | 41 | ## Support My Work 42 | 43 | If you find PlanCraft helpful and would like to support my work, you can buy me a coffee. Your support will help keep this project alive and thriving. It's a small token of appreciation that goes a long way. 44 | 45 | [![Buy me a coffee](https://cdn.buymeacoffee.com/buttons/default-orange.png)](https://www.buymeacoffee.com/realrashid) 46 | 47 | PlanCraft - Elevate Your Plans Management with PlanCraft for Laravel | Product Hunt 48 | 49 | ## License 50 | 51 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 52 | 53 |
54 |

Made with ❤️ from Pakistan

55 | -------------------------------------------------------------------------------- /src/Plan.php: -------------------------------------------------------------------------------- 1 | key = $key; 94 | $this->name = $name; 95 | $this->price = $price; 96 | $this->interval = $interval; 97 | $this->features = $features; 98 | $this->eligibilities = $eligibilities; 99 | $this->planId = $planId; 100 | } 101 | 102 | /** 103 | * Describe the plan. 104 | * 105 | * @return $this 106 | */ 107 | public function description(string $description) 108 | { 109 | $this->description = $description; 110 | 111 | return $this; 112 | } 113 | 114 | /** 115 | * Set the trial days for the plan. 116 | * 117 | * @return $this 118 | */ 119 | public function trialDays(?int $trialDays) 120 | { 121 | $this->trialDays = $trialDays; 122 | 123 | return $this; 124 | } 125 | 126 | /** 127 | * Set the monthly incentive text for the plan. 128 | * 129 | * @return $this 130 | */ 131 | public function monthlyIncentive(?string $incentive) 132 | { 133 | $this->monthlyIncentive = $incentive; 134 | 135 | return $this; 136 | } 137 | 138 | /** 139 | * Set the yearly incentive text for the plan. 140 | * 141 | * @return $this 142 | */ 143 | public function yearlyIncentive(?string $incentive) 144 | { 145 | $this->yearlyIncentive = $incentive; 146 | 147 | return $this; 148 | } 149 | 150 | /** 151 | * Get the JSON serializable representation of the object. 152 | * 153 | * @return array 154 | */ 155 | #[\ReturnTypeWillChange] 156 | public function jsonSerialize() 157 | { 158 | return [ 159 | 'key' => $this->key, 160 | 'name' => __($this->name), 161 | 'price' => $this->price, 162 | 'interval' => $this->interval, 163 | 'description' => __($this->description), 164 | 'features' => $this->features, 165 | 'eligibilities' => $this->eligibilities, 166 | 'planId' => $this->planId, 167 | 'trialDays' => $this->trialDays, 168 | 'monthlyIncentive' => $this->monthlyIncentive, 169 | 'yearlyIncentive' => $this->yearlyIncentive, 170 | ]; 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/Commands/InstallCommand.php: -------------------------------------------------------------------------------- 1 | option('force'); 31 | 32 | // Define the path to PlanCraftServiceProvider. 33 | $providerPath = app_path('Providers/PlanCraftServiceProvider.php'); 34 | 35 | // Check if PlanCraftServiceProvider is already installed. 36 | $providerInstalled = file_exists($providerPath); 37 | 38 | // If --force is used, and both config and provider are installed, ask for confirmation. 39 | if ($force && $providerInstalled) { 40 | if (! $this->confirm('PlanCraft scaffolding is already installed. Do you want to republish the scaffolding?')) { 41 | $this->info('Installation process aborted.'); 42 | 43 | return; 44 | } 45 | } 46 | 47 | // If provider is installed and their are different, ask for confirmation. 48 | if ($providerInstalled && ! $this->compareServiceProviders()) { 49 | if (! $this->confirm('The PlanCraftServiceProvider has been modified. Do you want to republish the scaffolding?')) { 50 | $this->info('Installation process aborted.'); 51 | 52 | return; 53 | } 54 | } 55 | 56 | // Publish PlanCraft Service Provider. 57 | $this->comment('Initiating the publication of PlanCraft Service Provider...'); 58 | $this->callSilent('vendor:publish', ['--tag' => 'plancraft-provider', '--force' => $force]); 59 | $this->info('PlanCraft Service Provider successfully published.'); 60 | 61 | // Register PlanCraft service provider in the application configuration file. 62 | $this->registerPlanCraftServiceProvider('RouteServiceProvider', 'PlanCraftServiceProvider'); 63 | 64 | $this->info('PlanCraft scaffolding successfully installed.'); 65 | 66 | $this->line(''); 67 | $this->comment('Now, execute the subsequent command to migrate your database:'); 68 | $this->line('php artisan migrate'); 69 | } 70 | 71 | /** 72 | * Registering the PlanCraft service provider in the application configuration file. 73 | */ 74 | protected function registerPlanCraftServiceProvider($after, $name): void 75 | { 76 | if (! Str::contains($appConfig = file_get_contents(config_path('app.php')), 'App\\Providers\\'.$name.'::class')) { 77 | file_put_contents(config_path('app.php'), str_replace( 78 | 'App\\Providers\\'.$after.'::class,', 79 | 'App\\Providers\\'.$after.'::class,'.PHP_EOL.' App\\Providers\\'.$name.'::class,', 80 | $appConfig 81 | )); 82 | } 83 | } 84 | 85 | /** 86 | * Compare the contents of the PlanCraftServiceProvider.stub with the installed PlanCraftServiceProvider. 87 | * 88 | * @return bool Returns true if the contents match, false otherwise. 89 | */ 90 | protected function compareServiceProviders(): bool 91 | { 92 | // Get the content of the PlanCraftServiceProvider.stub file from stubs directory. 93 | $stubContent = file_get_contents(__DIR__.'/../../stubs/app/Providers/PlanCraftServiceProvider.stub'); 94 | 95 | // Get the content of the installed PlanCraftServiceProvider. 96 | $installedProviderContent = file_get_contents(app_path('Providers/PlanCraftServiceProvider.php')); 97 | 98 | // Compare the content of the stub with the installed provider. 99 | return $stubContent === $installedProviderContent; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/PlanCraft.php: -------------------------------------------------------------------------------- 1 | key === $key || (is_array($plan->planId) && in_array($key, $plan->planId))) { 31 | return $plan; 32 | } 33 | } 34 | 35 | return null; 36 | } 37 | 38 | /** 39 | * Define a plan. 40 | * 41 | * @param string $key - The key of the plan. 42 | * @param string $name - The name of the plan. 43 | * @param string $price - The price of the plan (formatted as "monthly|yearly"). 44 | * @param string $interval - The billing interval of the plan (formatted as "Monthly|Yearly"). 45 | * @param array $features - Array of features associated with the plan. 46 | * @param array $eligibilities - Array of eligibility criteria for the plan. 47 | * @param string $planId - The plan's unique identifier used for payment processing (formatted as "price_monthly|price_yearly"). 48 | * @return RealRashid\PlanCraft\Plan - The newly created plan. 49 | */ 50 | public static function create(string $key, string $name, string $price, string $interval, array $features, array $eligibilities, ?string $planId = null) 51 | { 52 | // Ensure unique features across all plans 53 | static::$features = collect(array_merge(static::$features, $features)) 54 | ->unique() 55 | ->sort() 56 | ->values() 57 | ->all(); 58 | 59 | // Split the price and planId strings into arrays 60 | $priceArray = array_combine(explode('|', strtolower($interval)), explode('|', $price)); 61 | $planIdArray = $planId ? array_combine(explode('|', strtolower($interval)), explode('|', $planId)) : null; 62 | 63 | // Create a Price object to hold the prices 64 | $priceObject = (object) $priceArray; 65 | 66 | // Create a new Plan instance with the Price object and store it 67 | return tap(new Plan($key, $name, $priceObject, explode('|', $interval), $features, $eligibilities, $planIdArray), function ($plan) use ($key) { 68 | static::$plans[$key] = $plan; 69 | }); 70 | } 71 | 72 | /** 73 | * Get all defined plans with converted descriptions. 74 | * 75 | * @return array - Array of plans with descriptions. 76 | */ 77 | public static function plans(): array 78 | { 79 | return collect(static::$plans)->transform(function ($plan) { 80 | return with($plan->jsonSerialize(), function ($data) { 81 | return (new Plan( 82 | $data['key'], 83 | $data['name'], 84 | $data['price'], 85 | $data['interval'], 86 | $data['features'], 87 | $data['eligibilities'], 88 | $data['planId'], 89 | ))->description($data['description'])->trialDays($data['trialDays']) 90 | ->monthlyIncentive($data['monthlyIncentive']) 91 | ->yearlyIncentive($data['yearlyIncentive']); 92 | }); 93 | })->values()->all(); 94 | } 95 | 96 | /** 97 | * Check if there are any plans defined. 98 | * 99 | * @return bool - True if there are plans, false otherwise. 100 | */ 101 | public static function hasPlans(): bool 102 | { 103 | return count(static::$plans) > 0; 104 | } 105 | 106 | /** 107 | * Check if there are any features defined. 108 | * 109 | * @return bool - True if there are features, false otherwise. 110 | */ 111 | public static function hasFeatuers(): bool 112 | { 113 | return count(static::$features) > 0; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Trait/HasPlan.php: -------------------------------------------------------------------------------- 1 | morphOne(PlanModel::class, 'planable'); 25 | } 26 | 27 | /** 28 | * Check if the user currently has a specific plan. 29 | * 30 | * This method queries the database using the morphOne relationship to check if the user has 31 | * an associated plan with the provided $planKey. If a plan with the specified key is found, 32 | * the method returns true, indicating that the user has the plan. Otherwise, it returns false. 33 | * 34 | * @param string $planKey - The key of the plan to check for. 35 | * @return bool - True if the user has the plan, false otherwise. 36 | */ 37 | public function hasPlan(string $planKey): bool 38 | { 39 | return $this->plan()->where('key', $planKey)->exists(); 40 | } 41 | 42 | /** 43 | * Check if the user currently has an active plan. 44 | * 45 | * This method checks if there is an associated plan for the user using the morphOne 46 | * relationship. If a plan exists, it is considered active, and the method returns true. 47 | * Otherwise, if no plan is found, it is considered inactive, and the method returns false. 48 | * 49 | * @return bool - True if the user has an active plan, false otherwise. 50 | */ 51 | public function hasActivePlan(): bool 52 | { 53 | return $this->plan()->exists(); 54 | } 55 | 56 | /** 57 | * Find a plan based on a Plan Key or planId. 58 | * 59 | * This method serves as a bridge to the `findPlan` method in the `PlanCraft` class. It takes 60 | * a key or planId as input and attempts to locate a plan using the static `findPlan` method. 61 | * If a plan is found, it is returned as a `Plan` instance. If no plan matches the provided key 62 | * or planId, the method returns null. 63 | * 64 | * @param string $planKey - The key or planId of the plan to find. 65 | * @return RealRashid\PlanCraft\Plan|null - The corresponding Plan instance, or null if not found. 66 | */ 67 | public function findPlan($planKey): ?Plan 68 | { 69 | return PlanCraft::findPlan($planKey); 70 | } 71 | 72 | /** 73 | * Create a new plan for the user. 74 | * 75 | * This method attempts to create a new plan for the user. It first checks if the user 76 | * already has an existing plan associated with them. If a plan exists, the method returns 77 | * false, indicating that a new plan cannot be created. If no plan is associated with the 78 | * user, the method proceeds to find a plan based on the provided key or planId. If a valid 79 | * plan is found, a new plan record is created using the morphOne relationship, associating 80 | * it with the user. The method returns true to indicate a successful plan creation. 81 | * 82 | * @param string $planKeyOrId - The key or planId of the plan to create. 83 | * @return bool - True if the plan was created successfully, false otherwise. 84 | */ 85 | public function createPlan($planKeyOrId): bool 86 | { 87 | // Check if the user already has a plan 88 | if ($this->plan()->exists()) { 89 | return false; // User already has a plan, cannot create a new one 90 | } 91 | 92 | // Find the plan based on key or planId 93 | $plan = $this->findPlan($planKeyOrId); 94 | 95 | // Check if a valid plan was found 96 | if (! $plan) { 97 | return false; // Plan with the provided key or planId was not found 98 | } 99 | 100 | // Create a new plan using the morphOne relationship 101 | $this->plan()->create(['key' => $plan->key]); 102 | 103 | // Return true to indicate successful creation 104 | return true; 105 | } 106 | 107 | /** 108 | * Get the details of the user's current plan. 109 | * 110 | * This method retrieves the current plan associated with the user. If a plan is found, 111 | * it uses the 'key' attribute of the plan to fetch the complete plan details using the 112 | * PlanCraft::findPlan() method. 113 | * 114 | * @return RealRashid\PlanCraft\Plan|null - The user's current plan, or null if no plan is found. 115 | */ 116 | public function currentPlan(): ?Plan 117 | { 118 | // Retrieve the current plan associated with the user 119 | $plan = $this->plan; 120 | 121 | return $plan ? $this->findPlan($plan->key) : null; 122 | } 123 | 124 | /** 125 | * Delete the user's current plan. 126 | * 127 | * This method allows the user to delete their current associated plan. It first checks if 128 | * a plan is currently associated with the user. If a plan is found, it is deleted using 129 | * the `delete` method of the plan model, and the method returns true to indicate 130 | * successful deletion. If no plan is associated with the user, it returns false. 131 | * 132 | * @return bool - True if the plan was deleted successfully, false if no plan is associated. 133 | */ 134 | public function deletePlan(): bool 135 | { 136 | // Retrieve the current plan associated with the user 137 | $currentPlan = $this->plan; 138 | 139 | // Check if a plan was found 140 | if ($currentPlan) { 141 | // Delete the current plan 142 | $currentPlan->delete(); 143 | 144 | // Return true to indicate successful deletion 145 | return true; 146 | } 147 | 148 | // Return false if no plan is associated with the user 149 | return false; 150 | } 151 | 152 | /** 153 | * Get features of the user's current plan. 154 | * 155 | * This method retrieves the features associated with the user's current plan, if one exists. 156 | * It first checks if there is a current plan associated with the user. If a plan is found, 157 | * it uses the `findPlan` method to retrieve the plan details, including its features. 158 | * The method then returns an array of features. If no plan is associated, it returns null. 159 | * 160 | * @return array|null - An array of features or null if no plan is associated. 161 | */ 162 | public function getPlanFeatures(): ?array 163 | { 164 | $plan = $this->plan; 165 | 166 | return $plan ? $this->findPlan($plan->key)->features : null; 167 | } 168 | 169 | /** 170 | * Check if the user can access a specific feature. 171 | * 172 | * This method verifies whether the user has an active plan and if the specific feature 173 | * is accessible based on the plan's defined features. It returns a boolean value indicating 174 | * if the user can access the feature. 175 | * 176 | * @param string $feature - The name of the feature to check access for. 177 | * @return bool - True if the user can access the feature, false otherwise. 178 | */ 179 | public function canAccessFeature(string $feature): bool 180 | { 181 | return $this->plan && $this->checkSpecificFeatureAccess($feature); 182 | } 183 | 184 | /** 185 | * Check if the user is eligible to perform a specific action based on their subscription plan. 186 | * 187 | * This method determines if the user is eligible to perform a specific action based on their 188 | * subscription plan's configured eligibility limits. It takes an eligibility key and the count 189 | * of times the user has already performed the action. It returns a boolean value indicating 190 | * whether the user is eligible. 191 | * 192 | * @param string $eligibilityKey - The key of the eligibility to check eligibility for. 193 | * @param int $userCreated - The user's count for the specific eligibility limit (default is 0). 194 | * @return bool - True if the user is eligible, false otherwise. 195 | */ 196 | public function checkEligibility(string $eligibilityKey, int $userCreated = 0): bool 197 | { 198 | // Check if user has a valid plan 199 | $plan = $this->plan; 200 | if (! $plan) { 201 | return false; // User has no active plan, cannot perform the action 202 | } 203 | 204 | // Retrieve the plan configuration 205 | $planConfig = $this->findPlan($plan->key); 206 | 207 | if (! $planConfig) { 208 | return false; // Plan configuration not found, cannot determine eligibility 209 | } 210 | 211 | // Get the eligibility limit for the specified eligibilityKey 212 | $limit = $planConfig->eligibilities[$eligibilityKey] ?? null; 213 | 214 | if ($limit === null) { 215 | return false; // Eligibility limit not found, cannot determine eligibility 216 | } 217 | 218 | // Check if the limit is set to 0 (unlimited access) 219 | if ($limit === 0) { 220 | return true; 221 | } 222 | 223 | // Check if the user has reached the limit 224 | return $userCreated < $limit; 225 | } 226 | 227 | /** 228 | * Switch the user's current plan to a different plan identified by key or planId. 229 | * 230 | * This method allows the user to change their current plan to a new plan identified by either its key or planId. 231 | * It first checks if the new plan is different from the current plan. If they are the same, it 232 | * returns false to indicate that no switch was performed. If the new plan is found, it deletes 233 | * the current plan (if it exists) and creates a new plan with the specified key. 234 | * 235 | * @param string $newPlanKeyOrId - The key or planId of the new plan. 236 | * @return bool - True if the switch was successful, false otherwise. 237 | */ 238 | public function switchPlan(string $newPlanKeyOrId): bool 239 | { 240 | $currentPlan = $this->plan; 241 | 242 | if ($currentPlan && ($currentPlan->key === $newPlanKeyOrId || $currentPlan->planId === $newPlanKeyOrId)) { 243 | return false; // New plan is the same as the current plan 244 | } 245 | 246 | $newPlan = $this->findPlan($newPlanKeyOrId); 247 | 248 | if (! $newPlan) { 249 | return false; // New plan not found 250 | } 251 | 252 | if ($currentPlan) { 253 | $currentPlan->delete(); 254 | } 255 | 256 | $newPlan = new PlanModel(['key' => $newPlan->key]); 257 | $this->plan()->save($newPlan); 258 | 259 | return true; 260 | } 261 | 262 | /** 263 | * Check if the user has access to a specific feature. 264 | * 265 | * This method determines if the user has access to a specific feature based on their current plan. 266 | * It first retrieves the features associated with the user's plan. It then checks if the desired 267 | * feature is included in the list of plan features, or if there's a wildcard (*) that grants access 268 | * to all features. Additionally, it checks for specific wildcard cases with the help of the 269 | * `checkWildcardFeature` method. 270 | * 271 | * @param string $feature - The specific feature to check for access. 272 | * @return bool - True if the user has access, false otherwise. 273 | */ 274 | protected function checkSpecificFeatureAccess(string $feature): bool 275 | { 276 | $features = $this->getPlanFeatures(); 277 | 278 | return in_array($feature, $features) || 279 | in_array('*', $features) || 280 | $this->checkWildcardFeature($feature, $features); 281 | } 282 | 283 | /** 284 | * Check if a specific feature with wildcard is accessible. 285 | * 286 | * This method determines if a specific feature with a wildcard (*) is accessible for the user's current plan. 287 | * It checks if the feature ends with ':create' or ':update' and if there's a wildcard entry in the list of plan features 288 | * that grants access to all create or update actions. This method is used in conjunction with `checkSpecificFeatureAccess`. 289 | * 290 | * @param string $feature - The specific feature to check for wildcard access. 291 | * @param array $features - The list of features associated with the user's plan. 292 | * @return bool - True if the user has wildcard access to the feature, false otherwise. 293 | */ 294 | protected function checkWildcardFeature(string $feature, array $features): bool 295 | { 296 | return (Str::endsWith($feature, ':create') && in_array('*:create', $features)) || 297 | (Str::endsWith($feature, ':update') && in_array('*:update', $features)); 298 | } 299 | } 300 | --------------------------------------------------------------------------------