diff --git a/.editorconfig b/.editorconfig index c451dda..7d38cf2 100644 --- a/.editorconfig +++ b/.editorconfig @@ -14,5 +14,5 @@ trim_trailing_whitespace = true [*.md] trim_trailing_whitespace = false -[*.yml] +[*.{yml,yaml}] indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..cdac80f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +# Reduce Composer package download by removing obsolete files + +/.editorconfig export-ignore +/.gitattributes export-ignore +/.github export-ignore +/.gitignore export-ignore +/.php_cs.dist.php export-ignore +/CODE_OF_CONDUCT.md export-ignore +/CONTRIBUTING.md export-ignore +/phpunit.xml.dist export-ignore +/tests export-ignore diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.md b/.github/ISSUE_TEMPLATE/BUG_REPORT.md new file mode 100644 index 0000000..48f1395 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.md @@ -0,0 +1,19 @@ +--- +name: Bug report +about: Create a report to help us improve this project +title: '' +labels: bug +assignees: sebastiaanluca +--- + +### Description + +### Expected result + +### Steps to reproduce + +Please provide a fully working repository that reproduces the bug. + +### Additional info + +Logs, error output, setup, environment, packages, versions, etc. diff --git a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md new file mode 100644 index 0000000..60093d4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md @@ -0,0 +1,23 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: feature +assignees: sebastiaanluca +--- + +### Description + +A clear and concise description of the problem or proposal. + +### Suggested solution + +A clear and concise description of what you want to happen. + +### Possible alternatives + +A clear and concise description of any alternative solutions or features you've considered. + +### Additional context + +Any other context or screenshots to help situate and understand the requested feature. diff --git a/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md similarity index 73% rename from PULL_REQUEST_TEMPLATE.md rename to .github/PULL_REQUEST_TEMPLATE.md index 7ff85fa..8828020 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,3 +1,11 @@ +--- +name: Pull request +about: Create a new pull request to merge code into the main branch +title: 'A short, descriptive title' +labels: '' +assignees: sebastiaanluca +--- + ## PR Type What kind of pull request is this? Put an `x` in all the boxes that apply: @@ -8,17 +16,16 @@ What kind of pull request is this? Put an `x` in all the boxes that apply: - [ ] Change feature (non-breaking change which either changes or refactors existing functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) -## What does it change? - -Describe your changes in detail. +--- -## Why this PR? +### Description -Why is this change required? What problem does it solve? +Clearly describe what this pull request changes and why. -## How has this been tested? +### Steps to follow to verify functionality -Please describe in detail how you tested your changes (or are planning on testing them). +1. Clearly state which actions should be performed to fully and correctly review this issue. +2. … ## Checklist @@ -32,3 +39,4 @@ To facilitate merging your change and the approval of this PR, please make sure - If the change to the code requires a change to the documentation, it has been updated accordingly If you're unsure about any of these, don't hesitate to ask. We're here to help! + diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..dfd061a --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,54 @@ +name: Check code + +on: + push: + pull_request: + +jobs: + + check: + name: Run PHP tests - PHP ${{ matrix.php }} - ${{ matrix.dependency-version }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + php: [8.0, 8.1] + dependency-version: [prefer-lowest, prefer-stable] + os: [ubuntu-latest] + + steps: + - name: Check out code + uses: actions/checkout@v2 + + - name: Cache PHP dependencies + uses: actions/cache@v2 + with: + path: '**/vendor' + key: ${{ runner.os }}-vendor-cache-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-vendor-cache- + + - name: Cache Composer dependencies + uses: actions/cache@v2 + with: + path: ~/.composer/cache/files + key: composer-${{ runner.os }}-php-${{ matrix.php }}-${{ hashFiles('composer.json') }} + + - name: Validate Composer configuration file + run: composer validate --strict + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: mbstring + coverage: none + + - name: Install dependencies + run: composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-progress --optimize-autoloader + + - name: Lint code + run: vendor/bin/php-cs-fixer fix --dry-run --diff + + - name: Run tests + run: vendor/bin/phpunit diff --git a/.gitignore b/.gitignore index 5826402..28188ad 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ -/vendor -composer.phar +.idea +.php-cs-fixer.cache +.phpunit.result.cache composer.lock -.DS_Store +composer.phar +phpunit.xml +vendor/ diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..8db5cb0 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,242 @@ + true, + '@PSR2' => true, + '@PhpCsFixer' => true, + '@Symfony' => true, + '@PHP70Migration' => true, + '@PHP70Migration:risky' => true, + '@PHP71Migration' => true, + '@PHP71Migration:risky' => true, + '@PHP73Migration' => true, + '@PHPUnit75Migration:risky' => true, + 'final_class' => false, + 'new_with_braces' => false, + 'strict_comparison' => true, + 'list_syntax' => ['syntax' => 'short'], + 'mb_str_functions' => true, + 'class_attributes_separation' => [ + 'elements' => [ + 'method' => 'one', + ], + ], + 'no_extra_blank_lines' => [ + 'tokens' => [ + 'break', + 'continue', + 'curly_brace_block', + 'extra', + 'parenthesis_brace_block', + 'return', + 'square_brace_block', + 'throw', + 'use', + 'use_trait', + 'switch', + + 'case', + 'default', + ], + ], + 'no_blank_lines_before_namespace' => false, + 'nullable_type_declaration_for_default_null_value' => true, + 'increment_style' => ['style' => 'pre'], + 'self_static_accessor' => true, + 'static_lambda' => false, + 'no_empty_phpdoc' => true, + 'no_superfluous_phpdoc_tags' => [ + 'remove_inheritdoc' => true, + ], + 'phpdoc_line_span' => [ + 'const' => 'multi', + 'method' => 'multi', + 'property' => 'multi', + ], + 'general_phpdoc_tag_rename' => true, + 'phpdoc_add_missing_param_annotation' => ['only_untyped' => true], + 'phpdoc_align' => ['align' => 'left'], + 'phpdoc_indent' => true, + 'phpdoc_inline_tag_normalizer' => true, + 'phpdoc_no_access' => true, + 'phpdoc_no_empty_return' => false, + 'phpdoc_no_package' => true, + 'phpdoc_no_useless_inheritdoc' => true, + 'phpdoc_order' => true, + 'phpdoc_order_by_value' => false, + 'phpdoc_scalar' => true, + 'phpdoc_single_line_var_spacing' => true, + 'phpdoc_summary' => true, + 'phpdoc_tag_casing' => ['tags' => ['inheritDoc']], + 'phpdoc_tag_type' => true, + 'phpdoc_to_comment' => true, + 'phpdoc_trim' => true, + 'phpdoc_types' => ['groups' => ['simple', 'alias']], + 'phpdoc_types_order' => ['null_adjustment' => 'always_last'], + 'phpdoc_var_annotation_correct_order' => true, + 'phpdoc_var_without_name' => true, + 'align_multiline_comment' => ['comment_type' => 'phpdocs_like'], + 'php_unit_test_class_requires_covers' => false, + 'php_unit_internal_class' => false, + 'yoda_style' => false, + 'ordered_class_elements' => [ + 'order' => [ + 'use_trait', + 'constant_public', + 'constant_protected', + 'constant_private', + + 'property_static', + 'property_public_static', + 'property_protected_static', + 'property_private_static', + + 'property', + 'property_public', + 'property_protected', + 'property_private', + + 'construct', + 'destruct', + 'magic', + + 'method_static', + 'method_public_abstract_static', + 'method_public_static', + 'method_protected_abstract_static', + 'method_protected_static', + 'method_private_static', + 'method_public_abstract', + 'method_public', + 'method_protected_abstract', + 'method_protected', + 'method_private', + ], + ], + 'array_syntax' => ['syntax' => 'short'], + 'blank_line_after_namespace' => true, + 'blank_line_after_opening_tag' => true, + 'braces' => true, + 'cast_spaces' => true, + 'concat_space' => [ + 'spacing' => 'none', + ], + 'declare_equal_normalize' => true, + 'elseif' => true, + 'encoding' => true, + 'full_opening_tag' => true, + 'fully_qualified_strict_types' => true, // added by Shift + 'function_declaration' => true, + 'function_typehint_space' => true, + 'heredoc_to_nowdoc' => true, + 'include' => true, + 'indentation_type' => true, + 'linebreak_after_opening_tag' => true, + 'line_ending' => true, + 'lowercase_cast' => true, + 'lowercase_keywords' => true, + 'lowercase_static_reference' => true, // added from Symfony + 'magic_method_casing' => true, // added from Symfony + 'magic_constant_casing' => true, + 'method_argument_space' => true, + 'native_function_casing' => true, + 'no_alias_functions' => false, + 'no_blank_lines_after_class_opening' => true, + 'no_blank_lines_after_phpdoc' => true, + 'no_closing_tag' => true, + 'no_empty_statement' => true, + 'no_leading_import_slash' => true, + 'no_leading_namespace_whitespace' => true, + 'no_mixed_echo_print' => [ + 'use' => 'echo', + ], + 'no_multiline_whitespace_around_double_arrow' => true, + 'multiline_whitespace_before_semicolons' => [ + 'strategy' => 'no_multi_line', + ], + 'no_short_bool_cast' => true, + 'no_singleline_whitespace_before_semicolons' => true, + 'no_spaces_after_function_name' => true, + 'no_spaces_inside_parenthesis' => true, + 'no_trailing_comma_in_list_call' => false, + 'no_trailing_comma_in_singleline_array' => true, + 'no_trailing_whitespace' => true, + 'no_trailing_whitespace_in_comment' => true, + 'no_unreachable_default_argument_value' => true, + 'no_useless_return' => true, + 'no_whitespace_before_comma_in_array' => true, + 'no_whitespace_in_blank_line' => true, + 'normalize_index_brace' => true, + 'not_operator_with_successor_space' => true, + 'object_operator_without_whitespace' => true, + 'self_accessor' => false, + 'short_scalar_cast' => true, + 'simplified_null_return' => false, // disabled by Shift + 'single_blank_line_at_eof' => true, + 'single_blank_line_before_namespace' => true, + 'single_import_per_statement' => true, + 'single_line_after_imports' => true, + 'single_line_comment_style' => [ + 'comment_types' => ['hash'], + ], + 'single_quote' => true, + 'space_after_semicolon' => true, + 'standardize_not_equals' => true, + 'switch_case_semicolon_to_colon' => true, + 'switch_case_space' => true, + 'ternary_operator_spaces' => true, + 'trim_array_spaces' => true, + 'unary_operator_spaces' => true, + 'whitespace_after_comma_in_array' => true, + 'constant_case' => ['case' => 'lower'], + 'psr_autoloading' => true, + 'trailing_comma_in_multiline' => ['elements' => ['arrays']], + 'binary_operator_spaces' => [ + 'default' => 'single_space', + ], + 'types_spaces' => [ + 'space' => 'none', + ], + 'blank_line_before_statement' => [ + 'statements' => ['return'], + ], + 'class_definition' => [ + 'multi_line_extends_each_single_line' => true, + 'single_item_single_line' => true, + 'single_line' => true, + ], + 'ordered_imports' => [ + 'sort_algorithm' => 'alpha', + ], + 'no_unneeded_control_parentheses' => [ + 'statements' => ['break', 'clone', 'continue', 'echo_print', 'return', 'switch_case', 'yield'], + ], + 'no_spaces_around_offset' => [ + 'positions' => ['inside', 'outside'], + ], + 'visibility_required' => [ + 'elements' => ['property', 'method', 'const'], + ], +]; + +$finder = Finder::create() + ->in([ + __DIR__.'/src', + __DIR__.'/tests', + ]) + ->name('*.php') + ->notName('*.blade.php') + ->ignoreDotFiles(true) + ->ignoreVCS(true); + +return (new Config) + ->setRules($rules) + ->setFinder($finder) + ->setRiskyAllowed(true) + ->setUsingCache(true); diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 2b69546..0000000 --- a/.travis.yml +++ /dev/null @@ -1,44 +0,0 @@ -language: php - -php: - - 7.3 - - 7.4 - - nightly - -cache: - directories: - - $HOME/.composer/cache - -env: - matrix: - - LARAVEL_VERSION="^7.0" COMPOSER_FLAGS="--prefer-lowest" - - LARAVEL_VERSION="^7.0" COMPOSER_FLAGS="--prefer-stable" - - LARAVEL_VERSION="^8.0" COMPOSER_FLAGS="--prefer-lowest" - - LARAVEL_VERSION="^8.0" COMPOSER_FLAGS="--prefer-stable" - - LARAVEL_VERSION="dev-master" ORCHESTRA_VERSION="dev-master" COMPOSER_FLAGS="--prefer-lowest" MINIMUM_STABILITY="dev" - - LARAVEL_VERSION="dev-master" ORCHESTRA_VERSION="dev-master" COMPOSER_FLAGS="--prefer-stable" MINIMUM_STABILITY="dev" - -matrix: - allow_failures: - - php: nightly - - env: LARAVEL_VERSION="dev-master" ORCHESTRA_VERSION="dev-master" COMPOSER_FLAGS="--prefer-lowest" MINIMUM_STABILITY="dev" - - env: LARAVEL_VERSION="dev-master" ORCHESTRA_VERSION="dev-master" COMPOSER_FLAGS="--prefer-stable" MINIMUM_STABILITY="dev" - fast_finish: true - -before_install: - - composer validate --strict - - travis_retry composer self-update - - if [[ -n ${MINIMUM_STABILITY} ]]; then composer config minimum-stability ${MINIMUM_STABILITY}; echo "Minimum stability set to ${MINIMUM_STABILITY}"; else echo "Minimum stability left unchanged"; fi - - if [[ -n ${ORCHESTRA_VERSION} ]]; then composer require orchestra/testbench=${ORCHESTRA_VERSION} --dev --no-update; else echo "orchestra/testbench version requirement left unchanged"; fi - - composer require laravel/framework=${LARAVEL_VERSION} --no-update - -install: - - travis_retry composer update ${COMPOSER_FLAGS} --no-interaction --prefer-dist - -script: - - vendor/bin/phpunit - -notifications: - email: - on_failure: change - on_success: never diff --git a/CHANGELOG.md b/CHANGELOG.md index 122aeee..601908d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,27 @@ All Notable changes to `sebastiaanluca/laravel-boolean-dates` will be documented Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) principles. +## 6.0.0 (2022-03-13) + +⚠️ This release is a complete rewrite and changes the way it has to be used. Please consult the [README](README.md) for instructions. + +### Added + +- Added support for Laravel 9 + +### Changed + +- Switched to using `\Illuminate\Support\Carbon` and `\Carbon\CarbonImmutable` instead of `\Carbon\Carbon` +- Cleaned up code internally +- Added `BooleanDateAttribute` + +### Removed + +- Dropped support for PHP 7.x +- Dropped support for Laravel 7 and 8 +- Removed requirements for `nesbot/carbon` +- Removed `HasBooleanDates` trait + ## 5.0.0 (2020-10-19) ### Added diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md deleted file mode 100644 index 08bee51..0000000 --- a/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,24 +0,0 @@ -## Description - -Make it clear if the issue is a **bug**, an **enhancement** or just a **question**. The easiest way to indicate this is to prefix the title, e.g. `[Question] I have a question`. - -Provide a detailed description of the change or addition you are proposing. Include some screenshots or code examples if possible. - -### Your environment - -If you're reporting a bug or asking a specific question, include as many relevant details about your environment so we can reproduce it. The more, the better. - -- Package version or last commit -- Operating system and version -- PHP version -- Laravel version -- Related package versions -- … - -## 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. diff --git a/README.md b/README.md index 12eb9bd..31d7463 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Automatically convert Eloquent model boolean attributes to dates (and back) +# Convert Eloquent boolean attributes to dates (and back) [![Latest stable release][version-badge]][link-packagist] [![Software license][license-badge]](LICENSE.md) @@ -11,7 +11,7 @@ [![Follow @sebastiaanluca on Twitter][twitter-profile-badge]][link-twitter] [![Share this package on Twitter][twitter-share-badge]][link-twitter-share] -**A package to automatically convert boolean fields to dates (and back to booleans) so you always know when something was accepted or changed.** +**Automatically convert Eloquent model boolean fields to dates (and back to booleans)** so you always know _when_ something was accepted or changed. Say you've got a registration page for users where they need to accept your terms and perhaps can opt-in to certain features using checkboxes. With the new(-ish) GDPR privacy laws, you're somewhat required to not just keep track of the fact *if* they accepted those (or not), but also *when* they did. @@ -23,26 +23,19 @@ User registration controller: $input = request()->input(); $user = User::create([ - 'has_accepted_terms_and_conditions' => $input['terms'], - 'allows_data_processing' => $input['data_processing'], - 'has_agreed_to_something' => $input['something'], + 'has_accepted_terms' => $input['terms'], + 'is_subscribed_to_newsletter' => $input['newsletter'], ]); ``` Anywhere else in your code: ```php -$user->has_accepted_terms_and_conditions; +// true or false (boolean) +$user->has_accepted_terms; -/* - * true or false (boolean) - */ - -$user->accepted_terms_and_conditions_at; - -/* - * 2018-05-10 16:24:22 (Carbon instance) - */ +// 2018-05-10 16:24:22 (Carbon instance) +$user->accepted_terms_at; ``` ## Table of contents @@ -66,8 +59,8 @@ $user->accepted_terms_and_conditions_at; ## Requirements -- PHP 7.3 or higher -- Laravel 7.0 or higher +- PHP 8 or 8.1 +- Laravel ^9.2 ## How to install @@ -77,81 +70,118 @@ $user->accepted_terms_and_conditions_at; composer require sebastiaanluca/laravel-boolean-dates ``` -**Require the `HasBooleanDates` trait** in your Eloquent model, then add the `$booleanDates` field: +**Set up your Eloquent model** by: + +1. Adding your datetime columns to `$casts` +2. Adding the boolean attributes to `$appends` +3. Creating attribute accessors and mutators for each field ```php + */ + protected $casts = [ + 'accepted_terms_at' => 'immutable_datetime', + 'subscribed_to_newsletter_at' => 'datetime', + ]; + + /** + * The accessors to append to the model's array form. + * + * @var array */ - protected $booleanDates = [ - 'has_accepted_terms_and_conditions' => 'accepted_terms_at', - 'allows_data_processing' => 'accepted_processing_at', - 'has_agreed_to_something' => 'agreed_to_something_at', + protected $appends = [ + 'has_accepted_terms', + 'is_subscribed_to_newsletter', ]; + + protected function hasAcceptedTerms(): Attribute + { + return BooleanDateAttribute::for('accepted_terms_at'); + } + + protected function isSubscribedToNewsletter(): Attribute + { + return BooleanDateAttribute::for('subscribed_to_newsletter_at'); + } } ``` -To wrap up, create a **migration** to create a new or alter your existing table and add the timestamp fields: +Optionally, if your database table hasn't got the datetime columns yet, create a **migration** to create a new table or alter your existing table to add the timestamp fields: ```php timestamp('accepted_terms_at')->nullable(); - $table->timestamp('accepted_processing_at')->nullable(); - $table->timestamp('agreed_to_something_at')->nullable(); + $table->timestamp('subscribed_to_newsletter_at')->nullable(); }); } -} -``` +}; -Note: the related boolean fields are dynamic and do not need database fields. +``` ## How to use ### Saving dates -If a boolean date field's value is true, it'll be automatically converted to the current datetime: +If a boolean date field's value is true-ish, it'll be automatically converted to the current datetime. You can use anything like booleans, strings, positive integers, and so on. ```php $user = new User; // Setting values explicitly -$user->has_accepted_terms_and_conditions = true; -$user->allows_data_processing = 'yes'; +$user->has_accepted_terms = true; +$user->has_accepted_terms = 'yes'; +$user->has_accepted_terms = '1'; +$user->has_accepted_terms = 1; // Or using attribute filling -$user->fill([ - 'has_agreed_to_something' => 1, -]); +$user->fill(['is_subscribed_to_newsletter' => 'yes']); $user->save(); ``` All fields should now contain a datetime similar to `2018-05-10 16:24:22`. +Note that the date stored in the database column **is immutable, i.e. it's only set once**. Any following updates will not change the stored date(time), unless you update the date column manually or if you set it to `false` and back to `true` (disabling, then enabling it). + +For example: + +```php +$user = new User; + +$user->has_accepted_terms = true; +$user->save(); + +// `accepted_terms_at` column will contain `2022-03-13 13:20:00` + +$user->has_accepted_terms = true; +$user->save(); + +// `accepted_terms_at` column will still contain the original `2022-03-13 13:20:00` date +``` + ### Clearing saved values Of course you can also remove the saved date and time, for instance if a user retracts their approval: @@ -159,13 +189,12 @@ Of course you can also remove the saved date and time, for instance if a user re ```php $user = User::findOrFail(42); -$user->has_accepted_terms_and_conditions = false; -// $user->has_accepted_terms_and_conditions = null; - -$user->allows_data_processing = 0; -// $user->allows_data_processing = '0'; - -$user->has_agreed_to_something = ''; +$user->has_accepted_terms = false; +$user->has_accepted_terms = null; +$user->has_accepted_terms = '0'; +$user->has_accepted_terms = 0; +$user->has_accepted_terms = ''; +// $user->has_accepted_terms = null; $user->save(); ``` @@ -181,11 +210,8 @@ Use a boolean field's defined _key_ to access its boolean value: ```php $user = User::findOrFail(42); -$user->has_accepted_terms_and_conditions; - -/* - * true or false (boolean) - */ +// true or false (boolean) +$user->has_accepted_terms; ``` #### Retrieving fields as datetimes @@ -195,22 +221,16 @@ Use a boolean field's defined _value_ to explicitly access its (Carbon) datetime ```php $user = User::findOrFail(42); +// 2018-05-10 16:24:22 (Carbon or CarbonImmutable instance) $user->accepted_terms_at; -/* - * 2018-05-10 16:24:22 (Carbon instance) - */ - -$user->accepted_processing_at; - -/* - * NULL - */ +// null +$user->is_subscribed_to_newsletter; ``` ### Array conversion -When converting a model to an array, all boolean fields ánd their datetimes will be included: +When converting a model to an array, the boolean fields will be included if you've added them to the `$appends` array in your model. ```php $user = User::findOrFail(42); @@ -221,12 +241,10 @@ $user->toArray(); * Which will return something like: * * [ - * 'accepted_terms_at' => \Carbon\Carbon('2018-05-10 16:24:22'), - * 'accepted_processing_at' => NULL, - * 'agreed_to_something_at' => \Carbon\Carbon('2018-05-10 16:24:22'), - * 'accepted_terms_and_conditions' => true, - * 'allows_data_processing' => false, - * 'agreed_to_something' => true, + * 'accepted_terms_at' => \Carbon\CarbonImmutable('2018-05-10 16:24:22'), + * 'subscribed_to_newsletter_at' => \Illuminate\Support\Carbon('2018-05-10 16:24:22'), + * 'has_accepted_terms' => true, + * 'is_subscribed_to_newsletter' => true, * ]; */ ``` @@ -282,8 +300,8 @@ Have a project that could use some guidance? Send me an e-mail at [hello@sebasti [link-twitter-share]: https://twitter.com/intent/tweet?text=Easily%20convert%20Eloquent%20model%20booleans%20to%20dates%20and%20back%20with%20Laravel%20Boolean%20Dates.%20Via%20@sebastiaanluca%20https://github.com/sebastiaanluca/laravel-boolean-dates [link-contributors]: ../../contributors -[link-portfolio]: https://www.sebastiaanluca.com -[link-blog]: https://blog.sebastiaanluca.com +[link-portfolio]: https://sebastiaanluca.com +[link-blog]: https://sebastiaanluca.com/blog [link-packages]: https://packagist.org/packages/sebastiaanluca [link-twitter]: https://twitter.com/sebastiaanluca [link-github-profile]: https://github.com/sebastiaanluca diff --git a/composer.json b/composer.json index 7ec60cd..2ec80b4 100644 --- a/composer.json +++ b/composer.json @@ -11,6 +11,7 @@ "eloquent", "laravel", "model", + "php", "timestamps" ], "homepage": "https://github.com/sebastiaanluca/laravel-boolean-dates", @@ -19,21 +20,18 @@ { "name": "Sebastiaan Luca", "email": "hello@sebastiaanluca.com", - "homepage": "https://www.sebastiaanluca.com", + "homepage": "https://sebastiaanluca.com", "role": "Author" } ], "require": { - "php": "^7.3", - "laravel/framework": "^7.0|^8.0", - "nesbot/carbon": "^1.22|^2.0" + "php": "^8.0|^8.1", + "illuminate/database": "^9.2", + "illuminate/support": "^9.2" }, "require-dev": { - "dms/phpunit-arraysubset-asserts": "^0.1.0", - "kint-php/kint": "^3.3", - "mockery/mockery": "^1.3", - "orchestra/testbench": "^5.1|^6.0", - "phpunit/phpunit": "^8.5" + "friendsofphp/php-cs-fixer": "^3.7", + "phpunit/phpunit": "^9.5" }, "autoload": { "psr-4": { @@ -48,30 +46,25 @@ "config": { "sort-packages": true }, - "extra": { - "laravel": { - "providers": [ - "SebastiaanLuca\\BooleanDates\\BooleanDatesServiceProvider" - ] - } - }, + "minimum-stability": "dev", + "prefer-stable": true, "scripts": { - "composer-validate": "@composer validate --no-check-all --strict --ansi", + "composer:validate": "@composer validate --strict --ansi", "test": "vendor/bin/phpunit", - "test-lowest": [ - "composer update --prefer-lowest --prefer-dist --no-interaction --ansi", + "lint": "vendor/bin/php-cs-fixer fix --dry-run --diff --ansi", + "fix": "vendor/bin/php-cs-fixer fix --ansi", + "check": [ + "@composer:validate", + "@lint", "@test" ], - "test-stable": [ - "composer update --prefer-stable --prefer-dist --no-interaction --ansi", - "@test" + "check:lowest": [ + "composer update --prefer-lowest --prefer-dist --no-interaction --ansi", + "@check" ], - "check": [ - "@composer-validate", - "@test" + "check:stable": [ + "composer update --prefer-stable --prefer-dist --no-interaction --ansi", + "@check" ] - }, - "minimum-stability": "dev", - "prefer-stable": true + } } - diff --git a/config/flow.php b/config/flow.php deleted file mode 100644 index ca5d8ed..0000000 --- a/config/flow.php +++ /dev/null @@ -1,5 +0,0 @@ - - - - - ./src - ./tests - - - diff --git a/phpunit.xml b/phpunit.xml deleted file mode 100644 index 74a29bd..0000000 --- a/phpunit.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - ./tests/Feature - - - ./tests/Unit - - - - - src/ - - - diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..a753534 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,26 @@ + + + + + src/ + + + + + tests + + + diff --git a/src/BooleanDateAttribute.php b/src/BooleanDateAttribute.php new file mode 100644 index 0000000..3ad1847 --- /dev/null +++ b/src/BooleanDateAttribute.php @@ -0,0 +1,34 @@ + static::getBooleanDate($value, $attributes, $column), + set: static fn (mixed $value, array $attributes): array => static::setBooleanDate($value, $attributes, $column), + ); + } + + private static function getBooleanDate(mixed $value, array $attributes, string $column): bool + { + return array_key_exists($column, $attributes) && $attributes[$column] !== null; + } + + private static function setBooleanDate(mixed $value, array $attributes, string $column): array + { + // Only update the field if it's never been set before or when it's being "disabled" + if (! $value || ! array_key_exists($column, $attributes) || $attributes[$column] === null) { + return [$column => ! $value ? null : CarbonImmutable::now()]; + } + + return []; + } +} diff --git a/src/BooleanDatesServiceProvider.php b/src/BooleanDatesServiceProvider.php deleted file mode 100644 index b40da05..0000000 --- a/src/BooleanDatesServiceProvider.php +++ /dev/null @@ -1,11 +0,0 @@ -dates = array_unique( - array_merge( - $this->dates, - array_values($this->getBooleanDates()) - ) - ); - } - - /** - * Convert the model's attributes to an array. - * - * @return array - */ - public function attributesToArray() : array - { - $attributes = parent::attributesToArray(); - - $attributes = $this->addBooleanDateAttributesToArray($attributes); - - return $attributes; - } - - /** - * Get an attribute from the model. - * - * @param string $key - * - * @return mixed - */ - public function getAttribute($key) - { - if (! $key) { - return null; - } - - if ($this->hasBooleanDate($key)) { - return $this->getBooleanDate($key); - } - - return parent::getAttribute($key); - } - - /** - * Set a given attribute on the model. - * - * @param string $key - * @param mixed $value - * - * @return $this - */ - public function setAttribute($key, $value) - { - if ($this->hasBooleanDate($key)) { - $this->setBooleanDate($key, $value); - - return $this; - } - - return parent::setAttribute($key, $value); - } - - /** - * @return array - */ - public function getBooleanDates() : array - { - return $this->booleanDates ?? []; - } - - /** - * @return array - */ - protected function getBooleanDateAttributes() : array - { - return array_intersect_key( - $this->getAttributes(), - array_flip($this->getBooleanDates()) - ); - } - - /** - * @param mixed $key - * - * @return bool - */ - protected function getBooleanDate($key) : bool - { - return parent::getAttribute($this->getBooleanDateField($key)) !== null; - } - - /** - * @param string $key - * @param mixed $value - * - * @return void - */ - protected function setBooleanDate(string $key, $value) : void - { - // Only update the timestamp if the value is true and if it's not yet set - // or if the value is false and we need to unset the field. - if (! $value || ($value && $this->currentBooleanDateFieldValueIsNotYetSet($key))) { - // Set the value directly on the attributes array, don't use - // setAttribute, and don't receive $200. (\) (°,,,°) (/) - // This allows us to format and set the datetime ourselves, - // and makes using the $dates field optional. - $this->attributes[$this->getBooleanDateField($key)] = $this->getNewBooleanDateValue($value); - } - } - - /** - * @param string $key - * - * @return bool - */ - protected function hasBooleanDate(string $key) : bool - { - return array_key_exists($key, $this->getBooleanDates()); - } - - /** - * @param mixed $key - * - * @return bool - */ - protected function currentBooleanDateFieldValueIsNotYetSet($key) : bool - { - if (! array_key_exists($this->getBooleanDateField($key), $this->getAttributes())) { - return true; - } - - return parent::getAttribute($this->getBooleanDateField($key)) === null; - } - - /** - * @param mixed $key - * - * @return string - */ - protected function getBooleanDateField($key) : string - { - return $this->booleanDates[$key]; - } - - /** - * @param mixed $value - * - * @return string|null - */ - protected function getNewBooleanDateValue($value) : ?string - { - return $value ? $this->fromDateTime(Carbon::now()) : null; - } - - /** - * @param array $attributes - * - * @return array - */ - protected function addBooleanDateAttributesToArray(array $attributes) : array - { - foreach ($this->getBooleanDates() as $booleanField => $date) { - if (! array_key_exists($date, $attributes)) { - continue; - } - - $attributes[$booleanField] = $this->getBooleanDate($booleanField); - } - - return $attributes; - } -} diff --git a/tests/BooleanAttributeTest.php b/tests/BooleanAttributeTest.php new file mode 100644 index 0000000..75c253f --- /dev/null +++ b/tests/BooleanAttributeTest.php @@ -0,0 +1,278 @@ + 1, + 'subscribed_to_newsletter_at' => Carbon::now(), + ]); + + $this->assertInstanceOf(CarbonImmutable::class, $model->accepted_terms_at); + $this->assertTrue($model->has_accepted_terms); + + $this->assertInstanceOf(Carbon::class, $model->subscribed_to_newsletter_at); + $this->assertTrue($model->is_subscribed_to_newsletter); + } + + /** + * @test + */ + public function it can handle empty initial values(): void + { + $model = new TestModel; + + $this->assertNull($model->accepted_terms_at); + $this->assertFalse($model->has_accepted_terms); + } + + /** + * @test + */ + public function it can enable a boolean attribute(): void + { + $model = new TestModel; + + $model->has_accepted_terms = false; + + $this->assertNull($model->accepted_terms_at); + $this->assertFalse($model->has_accepted_terms); + + $model->has_accepted_terms = true; + + $this->assertInstanceOf(CarbonImmutable::class, $model->accepted_terms_at); + $this->assertTrue($model->has_accepted_terms); + } + + /** + * @test + */ + public function it can disable a boolean attribute(): void + { + $model = new TestModel; + + $model->has_accepted_terms = true; + + $this->assertInstanceOf(CarbonImmutable::class, $model->accepted_terms_at); + $this->assertTrue($model->has_accepted_terms); + + $model->has_accepted_terms = false; + + $this->assertNull($model->accepted_terms_at); + $this->assertFalse($model->has_accepted_terms); + } + + /** + * @test + */ + public function it cannot enable a boolean attribute twice and change its date(): void + { + Carbon::setTestNow('2022-03-13 14:31:00'); + + $model = new TestModel; + + $model->has_accepted_terms = true; + + $this->assertInstanceOf(CarbonImmutable::class, $model->accepted_terms_at); + $this->assertSame('2022-03-13 14:31:00', $model->accepted_terms_at->toDateTimeString()); + $this->assertTrue($model->has_accepted_terms); + + $model->has_accepted_terms = true; + + $this->assertInstanceOf(CarbonImmutable::class, $model->accepted_terms_at); + $this->assertSame('2022-03-13 14:31:00', $model->accepted_terms_at->toDateTimeString()); + $this->assertTrue($model->has_accepted_terms); + } + + /** + * @test + */ + public function it can enable a boolean attribute twice if it was disabled(): void + { + Carbon::setTestNow('2022-03-13 14:00:00'); + + $model = new TestModel; + + $model->is_subscribed_to_newsletter = true; + + $this->assertInstanceOf(Carbon::class, $model->subscribed_to_newsletter_at); + $this->assertSame('2022-03-13 14:00:00', $model->subscribed_to_newsletter_at->toDateTimeString()); + $this->assertTrue($model->is_subscribed_to_newsletter); + + Carbon::setTestNow('2022-03-20 15:00:00'); + + $model->is_subscribed_to_newsletter = false; + + $this->assertNull($model->subscribed_to_newsletter_at); + $this->assertFalse($model->is_subscribed_to_newsletter); + + $model->is_subscribed_to_newsletter = true; + + $this->assertInstanceOf(Carbon::class, $model->subscribed_to_newsletter_at); + $this->assertSame('2022-03-20 15:00:00', $model->subscribed_to_newsletter_at->toDateTimeString()); + $this->assertTrue($model->is_subscribed_to_newsletter); + } + + /** + * @test + */ + public function it can enable a boolean attribute from a boolean(): void + { + $model = new TestModel; + + $model->has_accepted_terms = true; + + $this->assertInstanceOf(CarbonImmutable::class, $model->accepted_terms_at); + + $this->assertSame( + Carbon::now()->format('Y-m-d H:i'), + $model->accepted_terms_at->format('Y-m-d H:i') + ); + } + + /** + * @test + */ + public function it can enable a boolean attribute from a string(): void + { + $model = new TestModel; + + $model->has_accepted_terms = 'yes'; + + $this->assertSame( + Carbon::now()->format('Y-m-d H:i'), + $model->accepted_terms_at->format('Y-m-d H:i') + ); + } + + /** + * @test + */ + public function it can enable a boolean attribute from an integer string(): void + { + $model = new TestModel; + + $model->has_accepted_terms = '1'; + + $this->assertSame( + Carbon::now()->format('Y-m-d H:i'), + $model->accepted_terms_at->format('Y-m-d H:i') + ); + } + + /** + * @test + */ + public function it can enable a boolean attribute from an integer(): void + { + $model = new TestModel; + + $model->is_subscribed_to_newsletter = 1; + + $this->assertSame( + Carbon::now()->format('Y-m-d H:i'), + $model->subscribed_to_newsletter_at->format('Y-m-d H:i') + ); + } + + /** + * @test + */ + public function it can disable a boolean attribute from a boolean(): void + { + $model = new TestModel; + + $model->allows_data_processing = false; + + $this->assertNull($model->accepted_processing_at); + } + + /** + * @test + */ + public function it can disable a boolean attribute from null(): void + { + $model = new TestModel; + + $model->has_agreed_to_something = null; + + $this->assertNull($model->agreed_to_something_at); + } + + /** + * @test + */ + public function it can disable a boolean attribute from an integer string(): void + { + $model = new TestModel; + + $model->has_accepted_terms = '0'; + + $this->assertNull($model->accepted_terms_at); + } + + /** + * @test + */ + public function it can disable a boolean attribute from an integer(): void + { + $model = new TestModel; + + $model->has_accepted_terms = 0; + + $this->assertNull($model->accepted_terms_at); + } + + /** + * @test + */ + public function it can disable a boolean attribute from an empty string(): void + { + $model = new TestModel; + + $model->has_accepted_terms = ''; + + $this->assertNull($model->accepted_terms_at); + } + + /** + * @test + */ + public function it returns all attributes(): void + { + Carbon::setTestNow('2018-01-01 10:42:06'); + + $model = new TestModel; + + $model->something = 'something'; + + $model->has_accepted_terms = 0; + $model->is_subscribed_to_newsletter = 1; + + $expected = [ + 'something' => 'something', + + 'accepted_terms_at' => null, + 'subscribed_to_newsletter_at' => '2018-01-01T10:42:06.000000Z', + + 'has_accepted_terms' => false, + 'is_subscribed_to_newsletter' => true, + ]; + + $this->assertSame( + $expected, + $model->toArray(), + ); + } +} diff --git a/tests/Feature/BooleanArrayTest.php b/tests/Feature/BooleanArrayTest.php deleted file mode 100644 index 835a55c..0000000 --- a/tests/Feature/BooleanArrayTest.php +++ /dev/null @@ -1,69 +0,0 @@ - 'accepted_terms_at', - 'allows_data_processing' => 'accepted_processing_at', - 'has_agreed_to_something' => 'agreed_to_something_at', - ]; - - $this->assertSame( - $expected, - $model->getBooleanDates() - ); - } - - /** - * @test - */ - public function it returns all attributes() : void - { - Carbon::setTestNow('2018-01-01 10:42:06'); - - $model = new TestModel; - - $model->something = 'something'; - $model->tested_at = Carbon::now(); - - $model->has_accepted_terms_and_conditions = false; - $model->allows_data_processing = true; - $model->has_agreed_to_something = '0'; - - $expected = [ - 'something' => 'something', - 'tested_at' => '2018-01-01 10:42:06', - - 'has_accepted_terms_and_conditions' => false, - 'allows_data_processing' => true, - 'has_agreed_to_something' => false, - - 'accepted_terms_at' => null, - 'accepted_processing_at' => '2018-01-01T10:42:06.000000Z', - 'agreed_to_something_at' => null, - ]; - - ArraySubsetAsserts::assertArraySubset( - $expected, - $model->toArray() - ); - } -} diff --git a/tests/Feature/BooleanAttributeTest.php b/tests/Feature/BooleanAttributeTest.php deleted file mode 100644 index 8b42b4e..0000000 --- a/tests/Feature/BooleanAttributeTest.php +++ /dev/null @@ -1,148 +0,0 @@ -assertNull($model->getAttribute(null)); - $this->assertNull($model->getAttribute(false)); - $this->assertNull($model->getAttribute('')); - $this->assertNull($model->getAttribute('0')); - } - - /** - * @test - */ - public function it leaves other attributes untouched() : void - { - $model = new TestModel; - - $model->something = 'something'; - - $this->assertSame( - 'something', - $model->something - ); - } - - /** - * @test - */ - public function it sets the date from a true boolean() : void - { - $model = new TestModel; - - $model->has_accepted_terms_and_conditions = true; - - $this->assertSame( - Carbon::now()->format('Y-m-d H:i'), - $model->accepted_terms_at->format('Y-m-d H:i') - ); - } - - /** - * @test - */ - public function it sets the date from a non empty string() : void - { - $model = new TestModel; - - $model->has_accepted_terms_and_conditions = 'yes'; - - $this->assertSame( - Carbon::now()->format('Y-m-d H:i'), - $model->accepted_terms_at->format('Y-m-d H:i') - ); - } - - /** - * @test - */ - public function it sets the date from a positive integer value() : void - { - $model = new TestModel; - - $model->has_agreed_to_something = 1; - - $this->assertSame( - Carbon::now()->format('Y-m-d H:i'), - $model->agreed_to_something_at->format('Y-m-d H:i') - ); - } - - /** - * @test - */ - public function it sets the date from a positive integer string value() : void - { - $model = new TestModel; - - $model->has_accepted_terms_and_conditions = '1'; - - $this->assertSame( - Carbon::now()->format('Y-m-d H:i'), - $model->accepted_terms_at->format('Y-m-d H:i') - ); - } - - /** - * @test - */ - public function it clears the date from a false boolean() : void - { - $model = new TestModel; - - $model->allows_data_processing = false; - - $this->assertNull($model->accepted_processing_at); - } - - /** - * @test - */ - public function it clears the date from null() : void - { - $model = new TestModel; - - $model->has_agreed_to_something = null; - - $this->assertNull($model->agreed_to_something_at); - } - - /** - * @test - */ - public function it clears the date from a false string value() : void - { - $model = new TestModel; - - $model->has_accepted_terms_and_conditions = '0'; - - $this->assertNull($model->accepted_terms_at); - } - - /** - * @test - */ - public function it clears the date from an empty string value() : void - { - $model = new TestModel; - - $model->has_accepted_terms_and_conditions = '0'; - - $this->assertNull($model->accepted_terms_at); - } -} diff --git a/tests/Feature/BooleanDateTest.php b/tests/Feature/BooleanDateTest.php deleted file mode 100644 index 1575981..0000000 --- a/tests/Feature/BooleanDateTest.php +++ /dev/null @@ -1,59 +0,0 @@ -tested_at = Carbon::now('+5 days'); - - $this->assertSame( - Carbon::now('+5 days')->format('Y-m-d H:i:s'), - $model->tested_at->format('Y-m-d H:i:s') - ); - } - - /** - * @test - */ - public function it returns a date object from a true attribute() : void - { - $model = new TestModel; - - $model->allows_data_processing = true; - - $this->assertInstanceOf( - Carbon::class, - $model->accepted_processing_at - ); - - $this->assertEquals( - Carbon::now()->format('Y-m-d H:i:s'), - $model->accepted_processing_at->format('Y-m-d H:i:s') - ); - } - - /** - * @test - */ - public function it returns null from a false attribute() : void - { - $model = new TestModel; - - $model->allows_data_processing = false; - - $this->assertNull($model->accepted_processing_at); - } -} diff --git a/tests/TestCase.php b/tests/TestCase.php index 0549332..34e1718 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -4,20 +4,8 @@ namespace SebastiaanLuca\BooleanDates\Tests; -use Orchestra\Testbench\TestCase as BaseTestCase; -use SebastiaanLuca\BooleanDates\BooleanDatesServiceProvider; +use PHPUnit\Framework\TestCase as BaseTestCase; class TestCase extends BaseTestCase { - /** - * @param \Illuminate\Foundation\Application $app - * - * @return array - */ - protected function getPackageProviders($app) : array - { - return [ - BooleanDatesServiceProvider::class, - ]; - } } diff --git a/tests/TestModel.php b/tests/TestModel.php new file mode 100644 index 0000000..7673bdb --- /dev/null +++ b/tests/TestModel.php @@ -0,0 +1,54 @@ + + */ + protected $casts = [ + 'id' => 'integer', + 'accepted_terms_at' => 'immutable_datetime', + 'subscribed_to_newsletter_at' => 'datetime', + ]; + + /** + * @var array + */ + protected $appends = [ + 'has_accepted_terms', + 'is_subscribed_to_newsletter', + ]; + + protected function hasAcceptedTerms(): Attribute + { + return BooleanDateAttribute::for('accepted_terms_at'); + } + + protected function isSubscribedToNewsletter(): Attribute + { + return BooleanDateAttribute::for('subscribed_to_newsletter_at'); + } +} diff --git a/tests/resources/TestModel.php b/tests/resources/TestModel.php deleted file mode 100644 index 690bcee..0000000 --- a/tests/resources/TestModel.php +++ /dev/null @@ -1,27 +0,0 @@ - 'internal_timestamp_field'`. - * - * @var array - */ - protected $booleanDates = [ - 'has_accepted_terms_and_conditions' => 'accepted_terms_at', - 'allows_data_processing' => 'accepted_processing_at', - 'has_agreed_to_something' => 'agreed_to_something_at', - ]; -}