From ea3e447637464700aad2e11f8448e97ebeac2225 Mon Sep 17 00:00:00 2001 From: elmarzouguidev Date: Mon, 16 Oct 2023 18:04:17 +0100 Subject: [PATCH] Force Update Same packages to Support Illuminate >=9.* --- composer.json | 3 +- packages/laravel-feature/.editorconfig | 15 + packages/laravel-feature/.gitattributes | 11 + .../.github/workflows/.github-actions.yml | 100 +++++ packages/laravel-feature/.gitignore | 5 + .../.phpunit.cache/test-results | 1 + packages/laravel-feature/CHANGELOG.md | 30 ++ packages/laravel-feature/CONDUCT.md | 74 ++++ packages/laravel-feature/CONTRIBUTING.md | 29 ++ packages/laravel-feature/ISSUE_TEMPLATE.md | 27 ++ packages/laravel-feature/LICENSE.md | 21 + .../laravel-feature/PULL_REQUEST_TEMPLATE.md | 43 ++ packages/laravel-feature/README.md | 410 ++++++++++++++++++ packages/laravel-feature/composer.json | 56 +++ packages/laravel-feature/phpunit.xml | 11 + packages/laravel-feature/phpunit.xml.bak | 20 + .../laravel-feature/src/Config/features.php | 58 +++ .../Command/ScanViewsForFeaturesCommand.php | 72 +++ .../src/Domain/Exception/FeatureException.php | 7 + .../src/Domain/FeatureManager.php | 85 ++++ .../src/Domain/Model/Feature.php | 52 +++ .../Repository/FeatureRepositoryInterface.php | 21 + .../laravel-feature/src/Facade/Feature.php | 30 ++ .../src/Featurable/Featurable.php | 26 ++ .../src/Featurable/FeaturableInterface.php | 8 + ...016_12_17_105737_create_features_table.php | 36 ++ ..._12_17_163450_create_featurables_table.php | 34 ++ .../laravel-feature/src/Model/Feature.php | 9 + .../src/Provider/FeatureServiceProvider.php | 82 ++++ .../Repository/EloquentFeatureRepository.php | 97 +++++ .../src/Service/FeaturesViewScanner.php | 85 ++++ .../Integration/Blade/BladeFeatureTest.php | 45 ++ .../EloquentFeatureRepositoryTest.php | 311 +++++++++++++ .../Service/FeaturesViewScannerTest.php | 41 ++ .../Service/test_folder/i_will_be_ignored.php | 7 + .../test_folder/subfolder/view2.blade.php | 15 + .../Service/test_folder/view.blade.php | 7 + packages/laravel-feature/tests/TestCase.php | 37 ++ .../tests/Unit/Domain/FeatureManagerTest.php | 241 ++++++++++ .../tests/Unit/Domain/FeatureTest.php | 55 +++ 40 files changed, 2316 insertions(+), 1 deletion(-) create mode 100644 packages/laravel-feature/.editorconfig create mode 100644 packages/laravel-feature/.gitattributes create mode 100644 packages/laravel-feature/.github/workflows/.github-actions.yml create mode 100644 packages/laravel-feature/.gitignore create mode 100644 packages/laravel-feature/.phpunit.cache/test-results create mode 100644 packages/laravel-feature/CHANGELOG.md create mode 100644 packages/laravel-feature/CONDUCT.md create mode 100644 packages/laravel-feature/CONTRIBUTING.md create mode 100644 packages/laravel-feature/ISSUE_TEMPLATE.md create mode 100644 packages/laravel-feature/LICENSE.md create mode 100644 packages/laravel-feature/PULL_REQUEST_TEMPLATE.md create mode 100644 packages/laravel-feature/README.md create mode 100644 packages/laravel-feature/composer.json create mode 100644 packages/laravel-feature/phpunit.xml create mode 100644 packages/laravel-feature/phpunit.xml.bak create mode 100644 packages/laravel-feature/src/Config/features.php create mode 100644 packages/laravel-feature/src/Console/Command/ScanViewsForFeaturesCommand.php create mode 100644 packages/laravel-feature/src/Domain/Exception/FeatureException.php create mode 100644 packages/laravel-feature/src/Domain/FeatureManager.php create mode 100644 packages/laravel-feature/src/Domain/Model/Feature.php create mode 100644 packages/laravel-feature/src/Domain/Repository/FeatureRepositoryInterface.php create mode 100644 packages/laravel-feature/src/Facade/Feature.php create mode 100644 packages/laravel-feature/src/Featurable/Featurable.php create mode 100644 packages/laravel-feature/src/Featurable/FeaturableInterface.php create mode 100644 packages/laravel-feature/src/Migration/2016_12_17_105737_create_features_table.php create mode 100644 packages/laravel-feature/src/Migration/2016_12_17_163450_create_featurables_table.php create mode 100644 packages/laravel-feature/src/Model/Feature.php create mode 100644 packages/laravel-feature/src/Provider/FeatureServiceProvider.php create mode 100644 packages/laravel-feature/src/Repository/EloquentFeatureRepository.php create mode 100644 packages/laravel-feature/src/Service/FeaturesViewScanner.php create mode 100644 packages/laravel-feature/tests/Integration/Blade/BladeFeatureTest.php create mode 100644 packages/laravel-feature/tests/Integration/Repository/EloquentFeatureRepositoryTest.php create mode 100644 packages/laravel-feature/tests/Integration/Service/FeaturesViewScannerTest.php create mode 100644 packages/laravel-feature/tests/Integration/Service/test_folder/i_will_be_ignored.php create mode 100644 packages/laravel-feature/tests/Integration/Service/test_folder/subfolder/view2.blade.php create mode 100644 packages/laravel-feature/tests/Integration/Service/test_folder/view.blade.php create mode 100644 packages/laravel-feature/tests/TestCase.php create mode 100644 packages/laravel-feature/tests/Unit/Domain/FeatureManagerTest.php create mode 100644 packages/laravel-feature/tests/Unit/Domain/FeatureTest.php diff --git a/composer.json b/composer.json index e2215de..b8a385c 100644 --- a/composer.json +++ b/composer.json @@ -81,7 +81,8 @@ "Database\\Seeders\\": "database/seeders/", "Watson\\Validating\\": "packages/tmp-watson-validating/src", "Rinvex\\Subscriptions\\": "packages/laravel-subscriptions/src", - "Rinvex\\Support\\": "packages/laravel-support/src" + "Rinvex\\Support\\": "packages/laravel-support/src", + "LaravelFeature\\": "packages/laravel-feature/src" } }, "autoload-dev": { diff --git a/packages/laravel-feature/.editorconfig b/packages/laravel-feature/.editorconfig new file mode 100644 index 0000000..cd8eb86 --- /dev/null +++ b/packages/laravel-feature/.editorconfig @@ -0,0 +1,15 @@ +; This file is for unifying the coding style for different editors and IDEs. +; More information at http://editorconfig.org + +root = true + +[*] +charset = utf-8 +indent_size = 4 +indent_style = space +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/packages/laravel-feature/.gitattributes b/packages/laravel-feature/.gitattributes new file mode 100644 index 0000000..51c99a5 --- /dev/null +++ b/packages/laravel-feature/.gitattributes @@ -0,0 +1,11 @@ +# Path-based git attributes +# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html + +# Ignore all test and documentation with "export-ignore". +/.gitattributes export-ignore +/.gitignore export-ignore +/.travis.yml export-ignore +/phpunit.xml.dist export-ignore +/.scrutinizer.yml export-ignore +/tests export-ignore +/docs export-ignore diff --git a/packages/laravel-feature/.github/workflows/.github-actions.yml b/packages/laravel-feature/.github/workflows/.github-actions.yml new file mode 100644 index 0000000..fe64d62 --- /dev/null +++ b/packages/laravel-feature/.github/workflows/.github-actions.yml @@ -0,0 +1,100 @@ +name: CI + +on: + push: + branches: + - master + - dev + tags: + - '*' + pull_request: + branches: [ master ] + + workflow_dispatch: + +jobs: + phpcs: + strategy: + matrix: + version: ['7.4', '8.1'] + runs-on: ubuntu-latest + + steps: + - name: Checkout the repository + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Setup PHP with composer v2 + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.version }} + tools: composer:v2 + + - name: Install composer packages + run: | + php -v + composer install --prefer-dist --no-ansi --no-interaction --no-progress --no-scripts + + - name: Execute PHP_CodeSniffer + run: | + php -v + composer check-style + + phpunit: + strategy: + matrix: + version: ['7.4', '8.1'] + runs-on: ubuntu-latest + + steps: + - name: Checkout the repository + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.version }} + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, mysql, mysqli, pdo_mysql, bcmath, intl, exif, iconv + coverage: xdebug + + - name: Install composer packages + run: | + php -v + composer install --prefer-dist --no-ansi --no-interaction --no-progress --no-scripts + + - name: Execute tests + run: | + php -v + ./vendor/phpunit/phpunit/phpunit --version + ./vendor/phpunit/phpunit/phpunit --coverage-clover=coverage.xml +# export CODECOV_TOKEN=${{ secrets.CODECOV_TOKEN }} +# bash <(curl -s https://codecov.io/bash) || echo 'Codecov failed to upload' + +# - name: Upload code coverage +# run: | +# export CODECOV_TOKEN=${{ secrets.CODECOV_TOKEN }} +# bash <(curl -s https://codecov.io/bash) || echo 'Codecov failed to upload' + + package-security-checker: + runs-on: ubuntu-latest + + steps: + - name: Checkout the repository + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + + - name: Install composer packages + run: | + php -v + composer install --prefer-dist --no-ansi --no-interaction --no-progress --no-scripts + + - name: Install security-checker + run: | + test -d local-php-security-checker || curl -L https://github.com/fabpot/local-php-security-checker/releases/download/v1.2.0/local-php-security-checker_1.2.0_linux_amd64 --output local-php-security-checker + chmod +x local-php-security-checker + ./local-php-security-checker diff --git a/packages/laravel-feature/.gitignore b/packages/laravel-feature/.gitignore new file mode 100644 index 0000000..2d2d775 --- /dev/null +++ b/packages/laravel-feature/.gitignore @@ -0,0 +1,5 @@ +.idea +build +composer.lock +vendor +.phpunit.result.cache diff --git a/packages/laravel-feature/.phpunit.cache/test-results b/packages/laravel-feature/.phpunit.cache/test-results new file mode 100644 index 0000000..6070f21 --- /dev/null +++ b/packages/laravel-feature/.phpunit.cache/test-results @@ -0,0 +1 @@ +{"version":1,"defects":{"LaravelFeature\\Tests\\Domain\\FeatureManagerTest::testAdd":8,"LaravelFeature\\Tests\\Domain\\FeatureManagerTest::testAddThrowsExceptionOnError":8,"LaravelFeature\\Tests\\Domain\\FeatureManagerTest::testRemove":8,"LaravelFeature\\Tests\\Domain\\FeatureManagerTest::testRemoveThrowsExceptionOnError":8,"LaravelFeature\\Tests\\Domain\\FeatureManagerTest::testRenameFeature":8,"LaravelFeature\\Tests\\Domain\\FeatureManagerTest::testRenameFeatureThrowsError":8,"LaravelFeature\\Tests\\Domain\\FeatureManagerTest::testEnableFeature":8,"LaravelFeature\\Tests\\Domain\\FeatureManagerTest::testDisableFeature":8,"LaravelFeature\\Tests\\Domain\\FeatureManagerTest::testFeatureIsEnabled":8,"LaravelFeature\\Tests\\Domain\\FeatureManagerTest::testEnableFor":8,"LaravelFeature\\Tests\\Domain\\FeatureManagerTest::testDisableFor":8,"LaravelFeature\\Tests\\Domain\\FeatureManagerTest::testIsEnabledFor":8},"times":{"LaravelFeature\\Tests\\Domain\\FeatureTest::testFeatureCreation":0,"LaravelFeature\\Tests\\Domain\\FeatureTest::testNameChange":0,"LaravelFeature\\Tests\\Domain\\FeatureTest::testEnable":0,"LaravelFeature\\Tests\\Domain\\FeatureTest::testDisable":0,"LaravelFeature\\Tests\\Integration\\Blade\\BladeFeatureTest::testFeatureStatementsAreCompiled":0.014,"LaravelFeature\\Tests\\Integration\\Blade\\BladeFeatureTest::testFeatureForStatementsAreCompiled":0.003,"LaravelFeature\\Tests\\Integration\\Repository\\EloquentFeatureRepositoryTest::testSave":0.014,"LaravelFeature\\Tests\\Integration\\Repository\\EloquentFeatureRepositoryTest::testSaveThrowsExceptionOnError":0.004,"LaravelFeature\\Tests\\Integration\\Repository\\EloquentFeatureRepositoryTest::testRemove":0.007,"LaravelFeature\\Tests\\Integration\\Repository\\EloquentFeatureRepositoryTest::testRemoveThrowsErrorOnFeatureNotFound":0.004,"LaravelFeature\\Tests\\Integration\\Repository\\EloquentFeatureRepositoryTest::testFindByName":0.003,"LaravelFeature\\Tests\\Integration\\Repository\\EloquentFeatureRepositoryTest::testFindByNameThrowsErrorOnFeatureNotFound":0.004,"LaravelFeature\\Tests\\Integration\\Repository\\EloquentFeatureRepositoryTest::testEnableFor":0.007,"LaravelFeature\\Tests\\Integration\\Repository\\EloquentFeatureRepositoryTest::testEnableForThrowsErrorOnFeatureNotFound":0.004,"LaravelFeature\\Tests\\Integration\\Repository\\EloquentFeatureRepositoryTest::testEnableForDoesNothingIfFeatureIsGloballyEnabled":0.004,"LaravelFeature\\Tests\\Integration\\Repository\\EloquentFeatureRepositoryTest::testDisableFor":0.006,"LaravelFeature\\Tests\\Integration\\Repository\\EloquentFeatureRepositoryTest::testDisableForThrowsErrorOnFeatureNotFound":0.005,"LaravelFeature\\Tests\\Integration\\Repository\\EloquentFeatureRepositoryTest::testDisableForDoesNothingIfFeatureIsGloballyEnabled":0.004,"LaravelFeature\\Tests\\Integration\\Repository\\EloquentFeatureRepositoryTest::testIsEnabledFor":0.005,"LaravelFeature\\Tests\\Integration\\Repository\\EloquentFeatureRepositoryTest::testIsEnabledForThrowsExceptionOnFeatureNotFound":0.004,"LaravelFeature\\Tests\\Integration\\Service\\FeaturesViewScannerTest::testServiceFindsFeaturesRight":0.005,"LaravelFeature\\Tests\\Domain\\FeatureManagerTest::testAdd":0.003,"LaravelFeature\\Tests\\Domain\\FeatureManagerTest::testAddThrowsExceptionOnError":0.001,"LaravelFeature\\Tests\\Domain\\FeatureManagerTest::testRemove":0.001,"LaravelFeature\\Tests\\Domain\\FeatureManagerTest::testRemoveThrowsExceptionOnError":0,"LaravelFeature\\Tests\\Domain\\FeatureManagerTest::testRenameFeature":0,"LaravelFeature\\Tests\\Domain\\FeatureManagerTest::testRenameFeatureThrowsError":0,"LaravelFeature\\Tests\\Domain\\FeatureManagerTest::testEnableFeature":0,"LaravelFeature\\Tests\\Domain\\FeatureManagerTest::testDisableFeature":0,"LaravelFeature\\Tests\\Domain\\FeatureManagerTest::testFeatureIsEnabled":0,"LaravelFeature\\Tests\\Domain\\FeatureManagerTest::testEnableFor":0,"LaravelFeature\\Tests\\Domain\\FeatureManagerTest::testDisableFor":0,"LaravelFeature\\Tests\\Domain\\FeatureManagerTest::testIsEnabledFor":0}} \ No newline at end of file diff --git a/packages/laravel-feature/CHANGELOG.md b/packages/laravel-feature/CHANGELOG.md new file mode 100644 index 0000000..fdedceb --- /dev/null +++ b/packages/laravel-feature/CHANGELOG.md @@ -0,0 +1,30 @@ +# Changelog + +All Notable changes to `laravel-feature` will be documented in this file. + +Updates follow the [Keep a CHANGELOG](http://keepachangelog.com/) principles. + +## [Unreleased] + +### Added +- Add method signature to PHPDoc of the Feature facade + +## 0.1.0 - 2016-12-18 + +### Added +- All the domain classes and interfaces for the features management; +- Eloquent concrete implementation of the FeatureRepositoryInterface; +- A trait to allow every model to be "featurable"; +- A command line tool to scan views for new features and save them; + +### Deprecated +- Nothing + +### Fixed +- Nothing + +### Removed +- Nothing + +### Security +- Nothing diff --git a/packages/laravel-feature/CONDUCT.md b/packages/laravel-feature/CONDUCT.md new file mode 100644 index 0000000..42ed909 --- /dev/null +++ b/packages/laravel-feature/CONDUCT.md @@ -0,0 +1,74 @@ +# Contributor Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or +advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at `francescomalatesta@live.it`. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/packages/laravel-feature/CONTRIBUTING.md b/packages/laravel-feature/CONTRIBUTING.md new file mode 100644 index 0000000..a0f0efc --- /dev/null +++ b/packages/laravel-feature/CONTRIBUTING.md @@ -0,0 +1,29 @@ +# Contributing + +Contributions are **welcome** and will be fully **credited**. + +We accept contributions via Pull Requests on [Github](https://github.com/francescomalatesta/laravel-feature). + +## Pull Requests + +- **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - Check the code style with ``$ composer check-style`` and fix it with ``$ composer fix-style``. + +- **Add tests!** - Your patch won't be accepted if it doesn't have tests. + +- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. + +- **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. + +- **Create feature branches** - Don't ask us to pull from your master branch. + +- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. + +- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. + +## Running Tests + +``` bash +$ composer test +``` + +**Happy coding**! diff --git a/packages/laravel-feature/ISSUE_TEMPLATE.md b/packages/laravel-feature/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..5b48c57 --- /dev/null +++ b/packages/laravel-feature/ISSUE_TEMPLATE.md @@ -0,0 +1,27 @@ + + +## Detailed description + +Provide a detailed description of the change or addition you are proposing. + +Make it clear if the issue is a bug, an enhancement or just a question. + +## Context + +Why is this change important to you? How would you use it? + +How can it benefit other users? + +## Possible implementation + +Not obligatory, but suggest an idea for implementing addition or change. + +## Your environment + +Include as many relevant details about the environment you experienced the bug in and how to reproduce it. + +* Version used (e.g. PHP 5.6, HHVM 3): +* Operating system and version (e.g. Ubuntu 16.04, Windows 7): +* Link to your project: +* ... +* ... diff --git a/packages/laravel-feature/LICENSE.md b/packages/laravel-feature/LICENSE.md new file mode 100644 index 0000000..8bec866 --- /dev/null +++ b/packages/laravel-feature/LICENSE.md @@ -0,0 +1,21 @@ +# The MIT License (MIT) + +Copyright (c) 2016 Francesco Malatesta + +> Permission is hereby granted, free of charge, to any person obtaining a copy +> of this software and associated documentation files (the "Software"), to deal +> in the Software without restriction, including without limitation the rights +> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +> copies of the Software, and to permit persons to whom the Software is +> furnished to do so, subject to the following conditions: +> +> The above copyright notice and this permission notice shall be included in +> all copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +> THE SOFTWARE. diff --git a/packages/laravel-feature/PULL_REQUEST_TEMPLATE.md b/packages/laravel-feature/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..86246b3 --- /dev/null +++ b/packages/laravel-feature/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,43 @@ + + +## Description + +Describe your changes in detail. + +## Motivation and context + +Why is this change required? What problem does it solve? + +If it fixes an open issue, please link to the issue here (if you write `fixes #num` +or `closes #num`, the issue will be automatically closed when the pull is accepted.) + +## How has this been tested? + +Please describe in detail how you tested your changes. + +Include details of your testing environment, and the tests you ran to +see how your change affects other areas of the code, etc. + +## Screenshots (if appropriate) + +## Types of changes + +What types of changes does your code introduce? Put an `x` in all the boxes that apply: +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to change) + +## Checklist: + +Go over all the following points, and put an `x` in all the boxes that apply. + +Please, please, please, don't send your pull request until all of the boxes are ticked. Once your pull request is created, it will trigger a build on our [continuous integration](http://www.phptherightway.com/#continuous-integration) server to make sure your [tests and code style pass](https://help.github.com/articles/about-required-status-checks/). + +- [ ] I have read the **[CONTRIBUTING](CONTRIBUTING.md)** document. +- [ ] My pull request addresses exactly one patch/feature. +- [ ] I have created a branch for this patch/feature. +- [ ] Each individual commit in the pull request is meaningful. +- [ ] I have added tests to cover my changes. +- [ ] If my change requires a change to the documentation, I have updated it accordingly. + +If you're unsure about any of these, don't hesitate to ask. We're here to help! diff --git a/packages/laravel-feature/README.md b/packages/laravel-feature/README.md new file mode 100644 index 0000000..8675a62 --- /dev/null +++ b/packages/laravel-feature/README.md @@ -0,0 +1,410 @@ +# Laravel-Feature + +[![Latest Stable Version](https://poser.pugx.org/francescomalatesta/laravel-feature/v/stable)](https://packagist.org/packages/francescomalatesta/laravel-feature) +[![Build Status](https://travis-ci.org/francescomalatesta/laravel-feature.svg?branch=master)](https://travis-ci.org/francescomalatesta/laravel-feature) +[![Code Coverage](https://scrutinizer-ci.com/g/francescomalatesta/laravel-feature/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/francescomalatesta/laravel-feature/?branch=master) +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/francescomalatesta/laravel-feature/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/francescomalatesta/laravel-feature/?branch=master) +[![StyleCI](https://styleci.io/repos/76716509/shield?branch=master)](https://styleci.io/repos/76716509) + +Laravel-Feature is a package fully dedicated to feature toggling in your application, in the easiest way. For Laravel, of course. + +It was inspired by the [AdEspresso Feature Flag Bundle](https://github.com/adespresso/FeatureBundle). + +## Feature-What? + +Feature toggling is basically a way to **have full control on the activation of a feature** in your applications. + +Let's make a couple of examples to give you an idea: + +* you just finished to work on the latest feature and you want to push it, but the marketing team wants you to deploy it in a second moment; +* the new killer-feature is ready, but you want to enable it only for a specific set of users; + +With Laravel-Feature, you can: + +* easily **define new features** in your application; +* **enable/disable features** globally; +* **enable/disable features for specific users**, or **for whatever you want**; + +There are many things to know about feature toggling: take a look to [this great article](http://martinfowler.com/articles/feature-toggles.html) for more info. It's a really nice and useful lecture. + +## Install + +You can install Laravel-Feature with Composer. + +``` bash +$ composer require francescomalatesta/laravel-feature +``` + +After that, you need to **add the `FeatureServiceProvider` to the `app.php` config file**. + +```php +... +LaravelFeature\Provider\FeatureServiceProvider::class, +... +``` + +Now you have to **run migrations**, to add the tables Laravel-Feature needs. + +```bash +$ php artisan migrate +``` + +... and you're good to go! + +### Facade + +If you want, you can also **add the `Feature` facade** to the `aliases` array in the `app.php` config file. + +```php +... +'Feature' => \LaravelFeature\Facade\Feature::class, +... +``` + +If you don't like Facades, **inject the `FeatureManager`** class wherever you want! + +### Config File + +By default, you can immediately use Laravel-Feature. However, if you want to tweak some settings, feel free to **publish the config file** with + +```bash +$ php artisan vendor:publish --provider="LaravelFeature\Provider\FeatureServiceProvider" +``` + +## Basic Usage + +There are two ways you can use features: working with them **globally** or **specifically for a specific entity**. + +### Globally Enabled/Disabled Features + +#### Declare a New Feature + +Let's say you have a new feature that you want to keep hidden until a certain moment. We will call it "page_code_cleaner". Let's **add it to our application**: + +```php +Feature::add('page_code_cleaner', false); +``` + +Easy, huh? As you can imagine, **the first argument is the feature name**. **The second is a boolean we specify to define the current status** of the feature. + +* `true` stands for **the feature is enabled for everyone**; +* `false` stands for **the feature is hidden, no one can use it/see it**; + +And that's all. + +#### Check if a Feature is Enabled + +Now, let's imagine a better context for our example. We're building a CMS, and our "page_code_cleaner" is used to... clean our HTML code. Let's assume we have a controller like this one. + +```php +class CMSController extends Controller { + public function getPage($pageSlug) { + + // here we are getting our page code from some service + $content = PageService::getContentBySlug($pageSlug); + + // here we are showing our page code + return view('layout.pages', compact('content')); + } +} +``` + +Now, we want to deploy the new service, but **we don't want to make it available for users**, because the marketing team asked us to release it the next week. LaravelFeature helps us with this: + +```php +class CMSController extends Controller { + public function getPage($pageSlug) { + + // here we are getting our page code from some service + $content = PageService::getContentBySlug($pageSlug); + + // feature flagging here! + if(Feature::isEnabled('page_code_cleaner')) { + $content = PageCleanerService::clean($content); + } + + // here we are showing our page code + return view('layout.pages', compact('content')); + } +} +``` + +Ta-dah! Now, **the specific service code will be executed only if the "page_code_cleaner" feature is enabled**. + +#### Change a Feature Activation Status + +Obviously, using the `Feature` class we can easily **toggle the feature activation status**. + +```php +// release the feature! +Feature::enable('page_code_cleaner'); + +// hide the feature! +Feature::disable('page_code_cleaner'); +``` + +#### Remove a Feature + +Even if it's not so used, you can also **delete a feature** easily with + +```php +Feature::remove('page_code_cleaner'); +``` + +Warning: *be sure about what you do. If you remove a feature from the system, you will stumble upon exceptions if checks for the deleted features are still present in the codebase.* + +#### Work with Views + +I really love blade directives, they help me writing more elegant code. I prepared **a custom blade directive, `@feature`**: + +```php +
This is an example template div. Always visible.
+ +@feature('my_awesome_feature') +

This paragraph will be visible only if "my_awesome_feature" is enabled!

+@endfeature + +
This is another example template div. Always visible too.
+``` + +A really nice shortcut! + +### Enable/Disable Features for Specific Users/Entities + +Even if the previous things we saw are useful, LaravelFeature **is not just about pushing the on/off button on a feature**. Sometimes, business necessities require more flexibility. Think about a [**Canary Release**](http://martinfowler.com/bliki/CanaryRelease.html): we want to rollout a feature only to specific users. Or, maybe, just for one tester user. + +#### Enable Features Management for Specific Users + +LaravelFeature makes this possible, and also easier just as **adding a trait to our `User` class**. + +In fact, all you need to do is to: + +* **add the `LaravelFeature\Featurable\Featurable` trait** to the `User` class; +* let the same class **implement the `FeaturableInterface` interface**; + +```php +... + +class User extends Authenticatable implements FeaturableInterface +{ + use Notifiable, Featurable; + +... +``` + +Nothing more! LaravelFeature now already knows what to do. + +#### Status Priority + +*Please keep in mind that all you're going to read from now is not valid if a feature is already enabled globally. To activate a feature for specific users, you first need to disable it.* + +Laravel-Feature **first checks if the feature is enabled globally, then it goes down at entity-level**. + +#### Enable/Disable a Feature for a Specific User + +```php +$user = Auth::user(); + +// now, the feature "my.feature" is enabled ONLY for $user! +Feature::enableFor('my.feature', $user); + +// now, the feature "my.feature" is disabled for $user! +Feature::disableFor('my.feature', $user); + +``` + +#### Check if a Feature is Enabled for a Specific User + +```php +$user = Auth::user(); + +if(Feature::isEnabledFor('my.feature', $user)) { + + // do amazing things! + +} +``` + +#### Other Notes + +LaravelFeature also provides a Blade directive to check if a feature is enabled for a specific user. You can use the `@featurefor` blade tags: +```php +@featurefor('my.feature', $user) + + // do $user related things here! + +@endfeaturefor +``` + +## Advanced Things + +Ok, now that we got the basics, let's raise the bar! + +### Enable Features Management for Other Entities + +As I told before, you can easily add features management for Users just by using the `Featurable` trait and implementing the `FeaturableInterface` in the User model. However, when structuring the relationships, I decided to implement a **many-to-many polymorphic relationship**. This means that you can **add feature management to any model**! + +Let's make an example: imagine that **you have a `Role` model** you use to implement a basic roles systems for your users. This because you have admins and normal users. + +So, **you rolled out the amazing killer feature but you want to enable it only for admins**. How to do this? Easy. Recap: + +* add the `Featurable` trait to the `Role` model; +* be sure the `Role` model implements the `FeaturableInterface`; + +Let's think the role-user relationship as one-to-many one. + +You will probably have a `role()` method on your `User` class, right? Good. You already know the rest: + +```php +// $role is the admin role! +$role = Auth::user()->role; + +... + +Feature::enableFor('my.feature', $role); + +... + +if(Feature::isEnabledFor('my.feature', $role)) { + + // this code will be executed only if the user is an admin! + +} +``` + +### Scan Directories for Features + +One of the nice bonuses of the package that inspired me when making this package, is the ability to **"scan" views, find `@feature` declarations and then add these scanned features if not already present** on the system. + +I created a simple **artisan command** to do this. + +```bash +$ php artisan feature:scan +``` + +The command will use a dedicated service to **fetch the `resources/views` folder and scan every single Blade view to find `@feature` directives**. It will then output the search results. + +Try it, you will like it! + +**Note:** if you have published the config file, you will be able to **change the list of scanned directories**. + +### Using a Custom Features Repository + +Imagine that you want to **change the place or the way you store features**. For some crazy reason, you want to store it on a static file, or on Dropbox. + +Now, Eloquent doesn't have a Dropbox driver, so you can't use this package. **Bye.** + +Just joking! When making this package, I wanted to be sure to create a fully reusable logic if the developer doesn't want to use Eloquent anymore. + +To do this, I created a nice interface for the Job, and created some bindings in the Laravel Service Container. Nothing really complex, anyway. + +The interface I am talking about is `FeatureRepositoryInterface`. + +```php + LaravelFeature\Repository\EloquentFeatureRepository::class +``` + +will become... + +```php +'repository' => My\Wonderful\DropboxFeatureRepository::class +``` + +Done! By the way, don't forget to let the entities you need to **implement the `FeaturableInterface`**. + +```php + + + + + tests/Unit + + + tests/Integration + + + diff --git a/packages/laravel-feature/phpunit.xml.bak b/packages/laravel-feature/phpunit.xml.bak new file mode 100644 index 0000000..68f7c6b --- /dev/null +++ b/packages/laravel-feature/phpunit.xml.bak @@ -0,0 +1,20 @@ + + + + + tests/Unit + + + tests/Integration + + + diff --git a/packages/laravel-feature/src/Config/features.php b/packages/laravel-feature/src/Config/features.php new file mode 100644 index 0000000..c24af02 --- /dev/null +++ b/packages/laravel-feature/src/Config/features.php @@ -0,0 +1,58 @@ + [ + base_path('resources/views') + ], + + /* + |-------------------------------------------------------------------------- + | Scanned Features Default Status + |-------------------------------------------------------------------------- + | + | When you use the feature:scan command, new features could be added to the + | system. Be default, this new features are disabled. You can change this + | by setting this value to true instead of false. + | + | By doing so, new added features will be automatically enabled globally. + | + */ + + 'scanned_default_enabled' => true, + + /* + |-------------------------------------------------------------------------- + | Features Repository + |-------------------------------------------------------------------------- + | + | Here you can configure the concrete class you will use to work with + | features. By default, this class is the EloquentFeatureRepository shipped + | with this package. As the name says, it works with Eloquent. + | + | However, you can use a custom feature repository if you want, just by + | creating a new class that implements the FeatureRepositoryInterface. + | + */ + + 'repository' => LaravelFeature\Repository\EloquentFeatureRepository::class + +]; diff --git a/packages/laravel-feature/src/Console/Command/ScanViewsForFeaturesCommand.php b/packages/laravel-feature/src/Console/Command/ScanViewsForFeaturesCommand.php new file mode 100644 index 0000000..6aab862 --- /dev/null +++ b/packages/laravel-feature/src/Console/Command/ScanViewsForFeaturesCommand.php @@ -0,0 +1,72 @@ +service = app()->make(FeaturesViewScanner::class); + } + + /** + * Execute the console command. + * + * @return mixed + */ + public function handle() + { + $features = $this->service->scan(); + $areEnabledByDefault = config('features.scanned_default_enabled'); + + $this->getOutput()->writeln(''); + + if (count($features) === 0) { + $this->error('No features were found in the project views!'); + $this->getOutput()->writeln(''); + return; + } + + $this->info(count($features) . ' features found in views:'); + $this->getOutput()->writeln(''); + + foreach ($features as $feature) { + $this->getOutput()->writeln('- ' . $feature); + } + + $this->getOutput()->writeln(''); + $this->info('All the new features were added to the database with the ' + . ($areEnabledByDefault ? 'ENABLED' : 'disabled') . + ' status by default. Nothing changed for the already present ones.'); + + $this->getOutput()->writeln(''); + } +} diff --git a/packages/laravel-feature/src/Domain/Exception/FeatureException.php b/packages/laravel-feature/src/Domain/Exception/FeatureException.php new file mode 100644 index 0000000..e6e4c71 --- /dev/null +++ b/packages/laravel-feature/src/Domain/Exception/FeatureException.php @@ -0,0 +1,7 @@ +repository = $repository; + } + + public function add($featureName, $isEnabled) + { + $feature = Feature::fromNameAndStatus($featureName, $isEnabled); + $this->repository->save($feature); + } + + public function remove($featureName) + { + $feature = $this->repository->findByName($featureName); + $this->repository->remove($feature); + } + + public function rename($featureOldName, $featureNewName) + { + /** @var Feature $feature */ + $feature = $this->repository->findByName($featureOldName); + $feature->setNewName($featureNewName); + + $this->repository->save($feature); + } + + public function enable($featureName) + { + /** @var Feature $feature */ + $feature = $this->repository->findByName($featureName); + + $feature->enable(); + + $this->repository->save($feature); + } + + public function disable($featureName) + { + /** @var Feature $feature */ + $feature = $this->repository->findByName($featureName); + + $feature->disable(); + + $this->repository->save($feature); + } + + public function isEnabled($featureName) + { + /** @var Feature $feature */ + $feature = $this->repository->findByName($featureName); + return $feature->isEnabled(); + } + + public function enableFor($featureName, FeaturableInterface $featurable) + { + $this->repository->enableFor($featureName, $featurable); + } + + public function disableFor($featureName, FeaturableInterface $featurable) + { + $this->repository->disableFor($featureName, $featurable); + } + + public function isEnabledFor($featureName, FeaturableInterface $featurable) + { + return $this->repository->isEnabledFor($featureName, $featurable); + } +} diff --git a/packages/laravel-feature/src/Domain/Model/Feature.php b/packages/laravel-feature/src/Domain/Model/Feature.php new file mode 100644 index 0000000..826e7da --- /dev/null +++ b/packages/laravel-feature/src/Domain/Model/Feature.php @@ -0,0 +1,52 @@ +name = $name; + $this->isEnabled = $isEnabled; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @return bool + */ + public function isEnabled() + { + return $this->isEnabled; + } + + public function setNewName($newName) + { + $this->name = $newName; + } + + public function enable() + { + $this->isEnabled = true; + } + + public function disable() + { + $this->isEnabled = false; + } +} diff --git a/packages/laravel-feature/src/Domain/Repository/FeatureRepositoryInterface.php b/packages/laravel-feature/src/Domain/Repository/FeatureRepositoryInterface.php new file mode 100644 index 0000000..078720f --- /dev/null +++ b/packages/laravel-feature/src/Domain/Repository/FeatureRepositoryInterface.php @@ -0,0 +1,21 @@ +first(); + + if ((bool) $model->is_enabled === true) { + return true; + } + + $feature = $this->features()->where('name', '=', $featureName)->first(); + return ($feature) ? true : false; + } + + public function features() + { + return $this->morphToMany(Feature::class, 'featurable'); + } +} diff --git a/packages/laravel-feature/src/Featurable/FeaturableInterface.php b/packages/laravel-feature/src/Featurable/FeaturableInterface.php new file mode 100644 index 0000000..4e71452 --- /dev/null +++ b/packages/laravel-feature/src/Featurable/FeaturableInterface.php @@ -0,0 +1,8 @@ +increments('id'); + + $table->string('name'); + $table->boolean('is_enabled'); + $table->timestamps(); + + $table->index('name'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('features'); + } +} diff --git a/packages/laravel-feature/src/Migration/2016_12_17_163450_create_featurables_table.php b/packages/laravel-feature/src/Migration/2016_12_17_163450_create_featurables_table.php new file mode 100644 index 0000000..cca6b88 --- /dev/null +++ b/packages/laravel-feature/src/Migration/2016_12_17_163450_create_featurables_table.php @@ -0,0 +1,34 @@ +bigIncrements('id'); + $table->integer('feature_id'); + + $table->integer('featurable_id'); + $table->string('featurable_type'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('featurables'); + } +} diff --git a/packages/laravel-feature/src/Model/Feature.php b/packages/laravel-feature/src/Model/Feature.php new file mode 100644 index 0000000..de762ca --- /dev/null +++ b/packages/laravel-feature/src/Model/Feature.php @@ -0,0 +1,9 @@ +loadMigrationsFrom(__DIR__.'/../Migration'); + + $this->publishes([ + __DIR__.'/../Config/features.php' => config_path('features.php'), + ]); + + $this->registerBladeDirectives(); + } + + /** + * Register any application services. + * + * @return void + */ + public function register() + { + $this->mergeConfigFrom(__DIR__.'/../Config/features.php', 'features'); + + $config = $this->app->make('config'); + + $this->app->bind(FeatureRepositoryInterface::class, function () use ($config) { + return app()->make($config->get('features.repository')); + }); + + $this->registerConsoleCommand(); + } + + private function registerBladeDirectives() + { + $this->registerBladeFeatureDirective(); + $this->registerBladeFeatureForDirective(); + } + + private function registerBladeFeatureDirective() + { + Blade::directive('feature', function ($featureName) { + return "isEnabled($featureName)): ?>"; + }); + + Blade::directive('endfeature', function () { + return ''; + }); + } + + private function registerBladeFeatureForDirective() + { + Blade::directive('featurefor', function ($args) { + return "isEnabledFor($args)): ?>"; + }); + + Blade::directive('endfeaturefor', function () { + return ''; + }); + } + + private function registerConsoleCommand() + { + if ($this->app->runningInConsole()) { + $this->commands([ + ScanViewsForFeaturesCommand::class + ]); + } + } +} diff --git a/packages/laravel-feature/src/Repository/EloquentFeatureRepository.php b/packages/laravel-feature/src/Repository/EloquentFeatureRepository.php new file mode 100644 index 0000000..02c2f17 --- /dev/null +++ b/packages/laravel-feature/src/Repository/EloquentFeatureRepository.php @@ -0,0 +1,97 @@ +getName())->first(); + + if (!$model) { + $model = new Model(); + } + + $model->name = $feature->getName(); + $model->is_enabled = $feature->isEnabled(); + + try { + $model->save(); + } catch (\Exception $e) { + throw new FeatureException('Unable to save the feature: ' . $e->getMessage()); + } + } + + public function remove(Feature $feature) + { + /** @var Model $model */ + $model = Model::where('name', '=', $feature->getName())->first(); + if (!$model) { + throw new FeatureException('Unable to find the feature.'); + } + + $model->delete(); + } + + public function findByName($featureName) + { + /** @var Model $model */ + $model = Model::where('name', '=', $featureName)->first(); + if (!$model) { + throw new FeatureException('Unable to find the feature.'); + } + + return Feature::fromNameAndStatus( + $model->name, + $model->is_enabled + ); + } + + public function enableFor($featureName, FeaturableInterface $featurable) + { + /** @var Model $model */ + $model = Model::where('name', '=', $featureName)->first(); + if (!$model) { + throw new FeatureException('Unable to find the feature.'); + } + + if ((bool) $model->is_enabled === true || $featurable->hasFeature($featureName) === true) { + return; + } + + $featurable->features()->attach($model->id); + } + + public function disableFor($featureName, FeaturableInterface $featurable) + { + /** @var Model $model */ + $model = Model::where('name', '=', $featureName)->first(); + if (!$model) { + throw new FeatureException('Unable to find the feature.'); + } + + if ((bool) $model->is_enabled === true || $featurable->hasFeature($featureName) === false) { + return; + } + + $featurable->features()->detach($model->id); + } + + public function isEnabledFor($featureName, FeaturableInterface $featurable) + { + /** @var Model $model */ + $model = Model::where('name', '=', $featureName)->first(); + if (!$model) { + throw new FeatureException('Unable to find the feature.'); + } + + return ($model->is_enabled) ? true : $featurable->hasFeature($featureName); + } +} diff --git a/packages/laravel-feature/src/Service/FeaturesViewScanner.php b/packages/laravel-feature/src/Service/FeaturesViewScanner.php new file mode 100644 index 0000000..f862f24 --- /dev/null +++ b/packages/laravel-feature/src/Service/FeaturesViewScanner.php @@ -0,0 +1,85 @@ +featureManager = $featureManager; + $this->config = $config; + } + + public function scan() + { + $pathsToBeScanned = $this->config->get('features.scanned_paths', [ 'resources/views' ]); + + $foundDirectives = []; + + foreach ($pathsToBeScanned as $path) { + $views = $this->getAllBladeViewsInPath($path); + + foreach ($views as $view) { + $foundDirectives = array_merge($foundDirectives, $this->getFeaturesForView($view)); + } + } + + $foundDirectives = array_unique($foundDirectives); + + foreach ($foundDirectives as $directive) { + $this->featureManager->add($directive, $this->config->get('features.scanned_default_enabled')); + } + + return $foundDirectives; + } + + private function getAllBladeViewsInPath($path) + { + $files = scandir($path); + $files = array_diff($files, ['..', '.']); + + $bladeViews = []; + + foreach ($files as $file) { + $itemPath = $path . DIRECTORY_SEPARATOR . $file; + + if (is_dir($itemPath)) { + $bladeViews = array_merge($bladeViews, $this->getAllBladeViewsInPath($itemPath)); + } + + if (is_file($itemPath) && Str::endsWith($file, '.blade.php')) { + $bladeViews[] = $itemPath; + } + } + + return $bladeViews; + } + + private function getFeaturesForView($view) + { + $fileContents = file_get_contents($view); + + preg_match_all('/@feature\(["\'](.+)["\']\)|@featurefor\(["\'](.+)["\']\,.*\)/', $fileContents, $results); + + return collect($results[1]) + ->merge($results[2]) + ->filter() + ->toArray(); + } +} diff --git a/packages/laravel-feature/tests/Integration/Blade/BladeFeatureTest.php b/packages/laravel-feature/tests/Integration/Blade/BladeFeatureTest.php new file mode 100644 index 0000000..b365831 --- /dev/null +++ b/packages/laravel-feature/tests/Integration/Blade/BladeFeatureTest.php @@ -0,0 +1,45 @@ +compiler = app(BladeCompiler::class); + } + + public function testFeatureStatementsAreCompiled() + { + $string = '@feature(\'feature.name\') +feature enabled +@endfeature'; + + $expected = 'isEnabled(\'feature.name\')): ?> +feature enabled +'; + + $this->assertEquals($expected, $this->compiler->compileString($string)); + } + + public function testFeatureForStatementsAreCompiled() + { + $string = '@featurefor(\'feature.name\', $user) +feature enabled +@endfeaturefor'; + + $expected = 'isEnabledFor(\'feature.name\', $user)): ?> +feature enabled +'; + + $this->assertEquals($expected, $this->compiler->compileString($string)); + } +} diff --git a/packages/laravel-feature/tests/Integration/Repository/EloquentFeatureRepositoryTest.php b/packages/laravel-feature/tests/Integration/Repository/EloquentFeatureRepositoryTest.php new file mode 100644 index 0000000..5a33f5a --- /dev/null +++ b/packages/laravel-feature/tests/Integration/Repository/EloquentFeatureRepositoryTest.php @@ -0,0 +1,311 @@ +repository = new EloquentFeatureRepository(); + } + + /** + * Tests the repository save operation. + */ + public function testSave() + { + $feature = Feature::fromNameAndStatus('my.feature', true); + + $this->repository->save($feature); + + $this->assertDatabaseHas('features', [ + 'name' => 'my.feature', + 'is_enabled' => true + ]); + } + + /** + * Tests that the save operation throws an exception if something goes wrong. + */ + public function testSaveThrowsExceptionOnError() + { + $feature = Feature::fromNameAndStatus(null, true); + + $this->expectException(FeatureException::class); + + $this->repository->save($feature); + } + + /** + * Tests that the removal operation goes well. + */ + public function testRemove() + { + $this->addTestFeature(); + + $feature = Feature::fromNameAndStatus('test.feature', true); + + $this->repository->remove($feature); + + $this->assertDatabaseMissing('features', [ + 'name' => 'test.feature' + ]); + } + + /** + * Tests the removal operation throws an exception if the feature is not found. + */ + public function testRemoveThrowsErrorOnFeatureNotFound() + { + $this->addTestFeature(); + + $feature = Feature::fromNameAndStatus('unknown.feature', true); + + $this->expectException(FeatureException::class); + $this->expectExceptionMessage('Unable to find the feature.'); + + $this->repository->remove($feature); + } + + /** + * Tests a feature is found. + */ + public function testFindByName() + { + $this->addTestFeature(); + + /** @var Feature $feature */ + $feature = $this->repository->findByName('test.feature'); + + $this->assertNotNull($feature); + } + + /** + * Tests an exception is thrown if the feature is not found. + */ + public function testFindByNameThrowsErrorOnFeatureNotFound() + { + $this->addTestFeature(); + + $this->expectException(FeatureException::class); + + /** @var Feature $feature */ + $this->repository->findByName('unknown.feature'); + } + + /** + * Tests the enable operation for a specific FeaturableInterface entity. + */ + public function testEnableFor() + { + $this->createTestEntityTable(); + + $entity = $this->addTestEntity(); + $feature = $this->addTestFeature(); + + $this->repository->enableFor('test.feature', $entity); + + $this->assertDatabaseHas('featurables', [ + 'feature_id' => $feature->id, + 'featurable_id' => $entity->id, + 'featurable_type' => get_class($entity) + ]); + + $this->dropTestEntityTable(); + } + + /** + * Tests the enable operation throws an error if the feature is not found. + */ + public function testEnableForThrowsErrorOnFeatureNotFound() + { + $this->createTestEntityTable(); + + $entity = $this->addTestEntity(); + $this->addTestFeature(); + + $this->expectException(FeatureException::class); + + $this->repository->enableFor('unknown.feature', $entity); + + $this->dropTestEntityTable(); + } + + /** + * Tests nothing happens if the feature is already enabled globally. + */ + public function testEnableForDoesNothingIfFeatureIsGloballyEnabled() + { + $this->createTestEntityTable(); + + $entity = $this->addTestEntity(); + $feature = $this->addTestFeature('test.feature', true); + + $this->repository->enableFor('test.feature', $entity); + + $this->assertDatabaseMissing('featurables', [ + 'feature_id' => $feature->id, + 'featurable_id' => $entity->id, + 'featurable_type' => get_class($entity) + ]); + + $this->dropTestEntityTable(); + } + + /** + * Tests the disable of a feature for a specific FeaturableInterface entity. + */ + public function testDisableFor() + { + $this->createTestEntityTable(); + + $entity = $this->addTestEntity(); + $feature = $this->addTestFeature(); + $this->enableTestFeatureOn($entity); + + $this->repository->disableFor('test.feature', $entity); + + $this->assertDatabaseMissing('featurables', [ + 'feature_id' => $feature->id, + 'featurable_id' => $entity->id, + 'featurable_type' => get_class($entity) + ]); + + $this->dropTestEntityTable(); + } + + /** + * Tests the disable operation throws an error if the feature is not found. + */ + public function testDisableForThrowsErrorOnFeatureNotFound() + { + $this->createTestEntityTable(); + + $entity = $this->addTestEntity(); + $this->addTestFeature(); + $this->enableTestFeatureOn($entity); + + $this->expectException(FeatureException::class); + + $this->repository->disableFor('unknown.feature', $entity); + + $this->dropTestEntityTable(); + } + + /** + * Tests nothing happens if the feature is already enabled globally. + */ + public function testDisableForDoesNothingIfFeatureIsGloballyEnabled() + { + $this->createTestEntityTable(); + + $entity = $this->addTestEntity(); + $feature = $this->addTestFeature('test.feature', true); + + $this->repository->disableFor('test.feature', $entity); + + $this->assertDatabaseMissing('featurables', [ + 'feature_id' => $feature->id, + 'featurable_id' => $entity->id, + 'featurable_type' => get_class($entity) + ]); + + $this->dropTestEntityTable(); + } + + /** + * Tests the enable status of a feature for a specific FeaturableInterface entity. + */ + public function testIsEnabledFor() + { + $this->createTestEntityTable(); + + $entity = $this->addTestEntity(); + $this->addTestFeature(); + $this->addTestFeature('second.feature'); + $this->enableTestFeatureOn($entity); + + $this->assertTrue($this->repository->isEnabledFor('test.feature', $entity)); + $this->assertFalse($this->repository->isEnabledFor('second.feature', $entity)); + + $this->dropTestEntityTable(); + } + + /** + * Tests an exception is thrown if the feature is not found. + */ + public function testIsEnabledForThrowsExceptionOnFeatureNotFound() + { + $this->createTestEntityTable(); + + $entity = $this->addTestEntity(); + + $this->expectException(FeatureException::class); + $this->expectExceptionMessage('Unable to find the feature.'); + + $this->assertTrue($this->repository->isEnabledFor('test.feature', $entity)); + + $this->dropTestEntityTable(); + } + + private function addTestFeature($name = 'test.feature', $isEnabled = false) + { + $feature = new FeatureModel; + + $feature->name = $name; + $feature->is_enabled = $isEnabled; + $feature->save(); + + return $feature; + } + + private function addTestEntity() + { + $entity = new FeaturableTestEntity(); + $entity->name = 'test-entity'; + $entity->save(); + + return $entity; + } + + private function createTestEntityTable() + { + \Schema::create('featurabletestentities', function (Blueprint $table) { + $table->increments('id'); + $table->string('name'); + $table->timestamps(); + }); + } + + private function dropTestEntityTable() + { + \Schema::drop('featurabletestentities'); + } + + private function enableTestFeatureOn($featurable) + { + $feature = \LaravelFeature\Model\Feature::first(); + $featurable->features()->attach($feature->id); + } +} + +class FeaturableTestEntity extends Model implements FeaturableInterface +{ + use Featurable; + protected $table = 'featurabletestentities'; +} diff --git a/packages/laravel-feature/tests/Integration/Service/FeaturesViewScannerTest.php b/packages/laravel-feature/tests/Integration/Service/FeaturesViewScannerTest.php new file mode 100644 index 0000000..e177328 --- /dev/null +++ b/packages/laravel-feature/tests/Integration/Service/FeaturesViewScannerTest.php @@ -0,0 +1,41 @@ +getMockBuilder(FeatureManager::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->service = new FeaturesViewScanner($managerMock, app()->make('config')); + } + + /** + * Tests the service is able to find features. + */ + public function testServiceFindsFeaturesRight() + { + $foundDirectives = $this->service->scan(); + + $this->assertCount(4, $foundDirectives); + $this->assertEquals([ + 'my.feature', + 'my.second_feature', + 'my.feature.for', + 'my.second_feature.for' + ], $foundDirectives); + } +} diff --git a/packages/laravel-feature/tests/Integration/Service/test_folder/i_will_be_ignored.php b/packages/laravel-feature/tests/Integration/Service/test_folder/i_will_be_ignored.php new file mode 100644 index 0000000..a779cf5 --- /dev/null +++ b/packages/laravel-feature/tests/Integration/Service/test_folder/i_will_be_ignored.php @@ -0,0 +1,7 @@ +@feature('ignored.feature') + :( +@endfeature + +@featurefor('ignored.feature.for', $user) + I am missing +@endfeaturefor \ No newline at end of file diff --git a/packages/laravel-feature/tests/Integration/Service/test_folder/subfolder/view2.blade.php b/packages/laravel-feature/tests/Integration/Service/test_folder/subfolder/view2.blade.php new file mode 100644 index 0000000..b72bca0 --- /dev/null +++ b/packages/laravel-feature/tests/Integration/Service/test_folder/subfolder/view2.blade.php @@ -0,0 +1,15 @@ +@feature('my.feature') + Hey! +@endfeature + +@feature('my.second_feature') + Yo! +@endfeature + +@featurefor('my.feature.for', $user) + Good day! +@endfeaturefor + +@featurefor('my.second_feature.for', $user) + Good day! +@endfeaturefor \ No newline at end of file diff --git a/packages/laravel-feature/tests/Integration/Service/test_folder/view.blade.php b/packages/laravel-feature/tests/Integration/Service/test_folder/view.blade.php new file mode 100644 index 0000000..63fefb3 --- /dev/null +++ b/packages/laravel-feature/tests/Integration/Service/test_folder/view.blade.php @@ -0,0 +1,7 @@ +@feature('my.feature') + Hello! +@endfeature + +@featurefor('my.feature.for', $user) + Hello again! +@endfeaturefor \ No newline at end of file diff --git a/packages/laravel-feature/tests/TestCase.php b/packages/laravel-feature/tests/TestCase.php new file mode 100644 index 0000000..12601bd --- /dev/null +++ b/packages/laravel-feature/tests/TestCase.php @@ -0,0 +1,37 @@ +loadMigrationsFrom(__DIR__.'/../src/Migration'); + } + + /** + * Define environment setup. + * + * @param \Illuminate\Foundation\Application $app + * + * @return void + */ + protected function getEnvironmentSetUp($app) + { + $app['config']->set('features.scanned_paths', [ __DIR__ . '/Integration/Service/test_folder' ]); + + $app['config']->set('database.default', 'testing'); + $app['config']->set('database.connections.testing', [ + 'driver' => 'sqlite', + 'database' => ':memory:', + 'prefix' => '', + ]); + } + + protected function getPackageProviders($app) + { + return [\LaravelFeature\Provider\FeatureServiceProvider::class]; + } +} diff --git a/packages/laravel-feature/tests/Unit/Domain/FeatureManagerTest.php b/packages/laravel-feature/tests/Unit/Domain/FeatureManagerTest.php new file mode 100644 index 0000000..93a177e --- /dev/null +++ b/packages/laravel-feature/tests/Unit/Domain/FeatureManagerTest.php @@ -0,0 +1,241 @@ +repositoryMock = $this->getMockBuilder(FeatureRepositoryInterface::class) + ->onlyMethods(['save', 'remove', 'findByName', 'enableFor', 'disableFor', 'isEnabledFor']) + ->disableOriginalConstructor() + ->getMock(); + + $this->manager = new FeatureManager($this->repositoryMock); + } + + /** + * Tests that everything goes well when adding a new feature to the system. + */ + public function testAdd() + { + $this->repositoryMock->expects($this->once()) + ->method('save'); + + $this->manager->add('my.feature', true); + } + + /** + * Tests an exception is thrown if something goes wrong during the saving of a new feature. + */ + public function testAddThrowsExceptionOnError() + { + $this->repositoryMock->expects($this->once()) + ->method('save') + ->willThrowException(new FeatureException('Unable to save the feature.')); + + $this->expectException(FeatureException::class); + $this->expectExceptionMessage('Unable to save the feature.'); + + $this->manager->add('my.feature', true); + } + + /** + * Tests that everything goes well during a feature removal. + */ + public function testRemove() + { + $feature = $this->getMockBuilder(Feature::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->repositoryMock->expects($this->once()) + ->method('findByName') + ->willReturn($feature); + + $this->repositoryMock->expects($this->once()) + ->method('remove'); + + $this->manager->remove('my.feature'); + } + + public function testRemoveThrowsExceptionOnError() + { + $feature = $this->getMockBuilder(Feature::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->repositoryMock->expects($this->once()) + ->method('findByName') + ->willReturn($feature); + + $this->repositoryMock->expects($this->once()) + ->method('remove') + ->willThrowException(new FeatureException('Unable to remove the feature.')); + + $this->expectException(FeatureException::class); + $this->expectExceptionMessage('Unable to remove the feature.'); + + $this->manager->remove('my.feature'); + } + + /** + * Tests that everything goes well during a feature rename. + */ + public function testRenameFeature() + { + $feature = $this->getMockBuilder(Feature::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->repositoryMock->expects($this->once()) + ->method('findByName') + ->willReturn($feature); + + $this->repositoryMock->expects($this->once()) + ->method('save'); + + $this->manager->rename('old.name', 'new.name'); + } + + /** + * Tests that an exception is thrown if the feature is not found. + */ + public function testRenameFeatureThrowsError() + { + $feature = $this->getMockBuilder(Feature::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->repositoryMock->expects($this->once()) + ->method('findByName') + ->willReturn($feature); + + $this->repositoryMock->expects($this->once()) + ->method('save') + ->willThrowException(new FeatureException('Unable to save the feature.')); + + $this->expectException(FeatureException::class); + $this->expectExceptionMessage('Unable to save the feature.'); + + $this->manager->rename('old.feature', 'new.feature'); + } + + /** + * Tests everything goes well when a feature is globally enabled. + */ + public function testEnableFeature() + { + $feature = $this->getMockBuilder(Feature::class) + ->disableOriginalConstructor() + ->getMock(); + + $feature->expects($this->once()) + ->method('enable'); + + $this->repositoryMock->expects($this->once()) + ->method('findByName') + ->willReturn($feature); + + $this->repositoryMock->expects($this->once()) + ->method('save'); + + $this->manager->enable('my.feature'); + } + + /** + * Tests everything goes well when a feature is globally disabled. + */ + public function testDisableFeature() + { + $feature = $this->getMockBuilder(Feature::class) + ->disableOriginalConstructor() + ->getMock(); + + $feature->expects($this->once()) + ->method('disable'); + + $this->repositoryMock->expects($this->once()) + ->method('findByName') + ->willReturn($feature); + + $this->repositoryMock->expects($this->once()) + ->method('save'); + + $this->manager->disable('my.feature'); + } + + /** + * Tests the manager can correctly check if a feature is enabled or not. + */ + public function testFeatureIsEnabled() + { + $feature = $this->getMockBuilder(Feature::class) + ->disableOriginalConstructor() + ->getMock(); + + $feature->expects($this->once()) + ->method('isEnabled') + ->willReturn(true); + + $this->repositoryMock->expects($this->once()) + ->method('findByName') + ->willReturn($feature); + + $this->assertTrue($this->manager->isEnabled('my.feature')); + } + + public function testEnableFor() + { + /** @var FeaturableInterface | \PHPUnit_Framework_MockObject_MockObject $featurableMock */ + $featurableMock = $this->getMockBuilder(FeaturableInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->repositoryMock->expects($this->once()) + ->method('enableFor'); + + $this->manager->enableFor('my.feature', $featurableMock); + } + + public function testDisableFor() + { + /** @var FeaturableInterface | \PHPUnit_Framework_MockObject_MockObject $featurableMock */ + $featurableMock = $this->getMockBuilder(FeaturableInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->repositoryMock->expects($this->once()) + ->method('disableFor'); + + $this->manager->disableFor('my.feature', $featurableMock); + } + + public function testIsEnabledFor() + { + /** @var FeaturableInterface | \PHPUnit_Framework_MockObject_MockObject $featurableMock */ + $featurableMock = $this->getMockBuilder(FeaturableInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->repositoryMock->expects($this->once()) + ->method('isEnabledFor'); + + $this->manager->isEnabledFor('my.feature', $featurableMock); + } +} diff --git a/packages/laravel-feature/tests/Unit/Domain/FeatureTest.php b/packages/laravel-feature/tests/Unit/Domain/FeatureTest.php new file mode 100644 index 0000000..637f757 --- /dev/null +++ b/packages/laravel-feature/tests/Unit/Domain/FeatureTest.php @@ -0,0 +1,55 @@ +assertEquals('my.feature', $feature->getName()); + $this->assertFalse($feature->isEnabled()); + } + + /** + * Tests the name change for a feature. + */ + public function testNameChange() + { + $feature = Feature::fromNameAndStatus('old.name', false); + $feature->setNewName('new.name'); + + $this->assertEquals('new.name', $feature->getName()); + } + + /** + * Tests the enable operation of a feature. + */ + public function testEnable() + { + $feature = Feature::fromNameAndStatus('my.feature', false); + + $feature->enable(); + + $this->assertTrue($feature->isEnabled()); + } + + /** + * Tests the disable operation of a feature. + */ + public function testDisable() + { + $feature = Feature::fromNameAndStatus('my.feature', true); + + $feature->disable(); + + $this->assertFalse($feature->isEnabled()); + } +}