├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .php-cs-fixer.php ├── README.md ├── composer.json ├── composer.lock ├── database ├── factories │ ├── DismissalFactory.php │ ├── DismissibleFactory.php │ ├── TestDismisserTypeOneFactory.php │ └── TestDismisserTypeTwoFactory.php └── migrations │ └── 2024_04_20_133407_create_dismissibles_for_laravel_tables.php ├── images └── dismissibles-for-laravel.jpg ├── phpstan.neon ├── phpunit.xml ├── src ├── Concerns │ └── Dismiss.php ├── Contracts │ └── Dismisser.php ├── DismissiblesServiceProvider.php ├── Facades │ └── Dismissibles.php ├── Models │ ├── Dismissal.php │ ├── Dismissible.php │ ├── TestDismisserTypeOne.php │ └── TestDismisserTypeTwo.php └── Traits │ └── HasDismissibles.php └── tests ├── BaseTestCase.php ├── Unit ├── Concerns │ └── DismissTest.php ├── Facades │ └── Dismissibles │ │ ├── DismissTest.php │ │ ├── GetAllForTest.php │ │ ├── GetTest.php │ │ ├── IsDismissedTest.php │ │ └── ShouldShowTest.php ├── Models │ ├── Dismissal │ │ ├── DismissedUntilTest.php │ │ ├── ExtraDataTest.php │ │ ├── ScopeDismissedAtTest.php │ │ └── ScopeDismissedByTest.php │ └── Dismissible │ │ ├── ActiveFromTest.php │ │ ├── ActiveUntilTest.php │ │ ├── IsDismissedByTest.php │ │ ├── ScopeActiveTest.php │ │ └── ScopeNotDismissedByTest.php └── Traits │ └── HasDismissibles │ └── DismissalsTest.php └── database └── migrations └── 2024_04_21_153207_create_test_dismissers_tables.php /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build-test: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v3 11 | - name: Setup PHP 12 | uses: shivammathur/setup-php@v2 13 | with: 14 | php-version: 8.2 15 | - name: Install backend dependencies 16 | run: composer install --no-ansi --no-interaction --no-scripts --no-progress 17 | - name: Run tests 18 | run: composer test 19 | - name: Run PHPStan 20 | run: composer phpstan -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | /vendor/ 3 | .phpunit.result.cache -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | setRiskyAllowed(true) 7 | ->setRules([ 8 | '@PSR12' => true, 9 | 'array_syntax' => ['syntax' => 'short'], 10 | 'class_attributes_separation' => ['elements' => ['method' => 'one']], 11 | 'multiline_whitespace_before_semicolons' => true, 12 | 'single_quote' => true, 13 | 'blank_line_after_opening_tag' => true, 14 | 'binary_operator_spaces' => [ 15 | 'operators' => [ 16 | '=>' => 'align_single_space_minimal', 17 | ], 18 | ], 19 | 'declare_strict_types' => true, 20 | 'blank_line_before_statement' => ['statements' => ['return']], 21 | 'cast_spaces' => true, 22 | 'concat_space' => ['spacing' => 'one'], 23 | 'declare_equal_normalize' => true, 24 | 'type_declaration_spaces' => ['elements' => ['function']], 25 | 'single_line_comment_style' => false, 26 | 'include' => true, 27 | 'lowercase_cast' => true, 28 | 'native_function_casing' => true, 29 | 'increment_style' => ['style' => 'post'], 30 | 'new_with_parentheses' => true, 31 | 'no_blank_lines_after_class_opening' => true, 32 | 'no_blank_lines_after_phpdoc' => true, 33 | 'no_empty_phpdoc' => true, 34 | 'no_empty_statement' => true, 35 | 'no_extra_blank_lines' => [ 36 | 'tokens' => [ 37 | 'curly_brace_block', 38 | 'extra', 39 | 'parenthesis_brace_block', 40 | 'square_brace_block', 41 | 'throw', 42 | 'use', 43 | ], 44 | ], 45 | 'ordered_imports' => true, 46 | 'no_leading_import_slash' => true, 47 | 'no_leading_namespace_whitespace' => true, 48 | 'no_mixed_echo_print' => ['use' => 'echo'], 49 | 'no_multiline_whitespace_around_double_arrow' => true, 50 | 'no_short_bool_cast' => true, 51 | 'no_singleline_whitespace_before_semicolons' => true, 52 | 'no_spaces_around_offset' => true, 53 | 'no_trailing_comma_in_singleline' => true, 54 | 'no_unneeded_control_parentheses' => true, 55 | 'no_unused_imports' => true, 56 | 'no_useless_else' => true, 57 | 'no_useless_return' => true, 58 | 'no_whitespace_before_comma_in_array' => true, 59 | 'no_whitespace_in_blank_line' => true, 60 | 'normalize_index_brace' => true, 61 | 'object_operator_without_whitespace' => true, 62 | 'phpdoc_align' => true, 63 | 'phpdoc_annotation_without_dot' => true, 64 | 'phpdoc_indent' => true, 65 | 'phpdoc_no_access' => true, 66 | 'phpdoc_no_alias_tag' => true, 67 | 'phpdoc_no_empty_return' => false, 68 | 'phpdoc_no_package' => true, 69 | 'phpdoc_no_useless_inheritdoc' => true, 70 | 'phpdoc_return_self_reference' => true, 71 | 'phpdoc_scalar' => true, 72 | 'phpdoc_separation' => [ 73 | 'groups' => [ 74 | // general 75 | [ 76 | 'deprecated', 77 | 'internal', 78 | 'todo', 79 | ], 80 | // doc blocks, phpstan types 81 | [ 82 | 'template', 83 | 'extends', 84 | 'mixin', 85 | 'phpstan-template', 86 | 'phpstan-extends', 87 | 'phpstan-param', 88 | 'param', 89 | 'param-out', 90 | 'phpstan-var', 91 | 'var', 92 | 'return', 93 | 'phpstan-return', 94 | 'property', 95 | 'property-read', 96 | 'property-write', 97 | 'method', 98 | ], 99 | // phpunit specific 100 | [ 101 | 'test', 102 | 'dataProvider', 103 | 'covers', 104 | 'coversDefaultClass', 105 | 'coversNothing', 106 | 'depends', 107 | 'requires', 108 | ], 109 | ], 110 | ], 111 | 'phpdoc_single_line_var_spacing' => true, 112 | 'phpdoc_summary' => true, 113 | 'phpdoc_to_comment' => false, 114 | 'phpdoc_trim' => true, 115 | 'phpdoc_types' => true, 116 | 'phpdoc_var_without_name' => false, 117 | 'self_accessor' => true, 118 | 'short_scalar_cast' => true, 119 | 'single_class_element_per_statement' => true, 120 | 'space_after_semicolon' => true, 121 | 'standardize_not_equals' => true, 122 | 'trailing_comma_in_multiline' => ['elements' => ['arrays']], 123 | 'trim_array_spaces' => true, 124 | 'whitespace_after_comma_in_array' => true, 125 | ]) 126 | ->setLineEnding("\n"); 127 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 📣 Dismissibles for Laravel 2 | 3 | ![Dismissibles for Laravel](./images/dismissibles-for-laravel.jpg) 4 | 5 | A Laravel package for easily managing the visibility of your recurring, dismissible objects like popups/notifications/modals on the backend. This package does not include frontend components, so it's compatible with any frontend you can use. 6 | 7 | ## 📕 Table of Contents 8 | 9 | - [✅ What problem does this solve?](#-what-problem-does-this-solve) 10 | - [📦 Installation](#-installation) 11 | - [❓ How to use](#-how-to-use) 12 | - [❗ Good to know](#-good-to-know) 13 | - [💾 Database tables](#-database-tables) 14 | - [☕ Buy me a coffee](#-buy-me-a-coffee) 15 | 16 | ## ✅ What problem does this solve? 17 | Say you have a popup you want to show to every user, daily for a week. Users can dismiss it and it should not show up again for the rest of the day until the next day. 18 | 19 | This packages handles the complex logic regarding whether the (dismissible) popup should be visible to the current user at the current moment. It basically handles the visibility of your dismissible. It's highly customizable, making it very flexible for many scenario's. 20 | 21 | Because it's serverside we can easily get statistics like who dismissed what, when and where. 22 | 23 | ## 📦 Installation 24 | 1. Require the package in your Laravel application 25 | ```shell 26 | composer require rellix/dismissibles-for-laravel 27 | ``` 28 | 29 | 2. Run the migrations to create the database tables 30 | ```shell 31 | php artisan migrate 32 | ``` 33 | 34 | ## ❓ How to use 35 | 36 | ### 1. Add the interface and trait to any model 37 | ```php 38 | use Rellix\Dismissibles\Contracts\Dismisser; 39 | use Rellix\Dismissibles\Traits\HasDismissibles; 40 | 41 | class User implements Dismisser 42 | { 43 | use HasDismissibles; 44 | 45 | ... 46 | } 47 | 48 | ``` 49 | 50 | ### 2. Create a dismissible (migration) 51 | ```php 52 | use Illuminate\Support\Facades\Date; 53 | use Illuminate\Support\Facades\DB; 54 | 55 | return new class () extends Migration { 56 | public function up(): void 57 | { 58 | DB::table('dismissibles')->insert([ 59 | 'name' => 'Test Popup', // This is your **unique** identifier 60 | 'active_from' => Date::createFromFormat('d-m-Y', '01-03-2024'), 61 | 'active_until' => null, // Optional end date 62 | 'created_at' => Date::now(), 63 | 'updated_at' => Date::now(), 64 | ]); 65 | } 66 | }; 67 | ``` 68 | 69 | and run your created migration: 70 | ```php 71 | php artisan migrate 72 | ``` 73 | 74 |
75 | 76 | 💡 You can also create/fetch a Dismissible inline using the "active"-scope and "firstOrCreate". 77 | 78 | ```php 79 | Dismissible::active()->firstOrCreate( 80 | ['name' => 'Test Popup'], 81 | [ 82 | 'active_from' => Date::createFromFormat('d-m-Y', '01-03-2024'), 83 | 'active_until' => null, 84 | 'created_at' => Date::now(), 85 | 'updated_at' => Date::now(), 86 | ] 87 | ); 88 | ``` 89 | 90 |
91 | 92 | ### 3. Check if it should be visible at the current moment 93 | ```php 94 | use Rellix\Dismissibles\Facades\Dismissibles; 95 | 96 | $showPopup = Dismissibles::shouldBeVisible('Test Popup', $user); 97 | 98 | // Here are some more examples, including ones with additional conditionals: 99 | $showPopup = Dismissibles::shouldBeVisible('Happy New Year 2025 Popup', $user); 100 | $showPopup = Dismissibles::shouldBeVisible('Newsletter signup modal', $user) && !$user->is_subscribed; 101 | $showPopup = Dismissibles::shouldBeVisible('Complete your profile notification', $user) && !$user->has_completed_profile; 102 | $showPopup = Dismissibles::shouldBeVisible('50% Off First Purchase Popup', $user) && !$user->has_orders; 103 | 104 | // You can also get all Dismissibles in one query (performance) and use the model methods. 105 | $dismissibles = Dismissibles::getAllFor($user); 106 | 107 | ``` 108 | 109 |
110 | 111 | 💡 You can also use the individual models. 112 | 113 | ```php 114 | use Rellix\Dismissibles\Facades\Dismissibles; 115 | 116 | $popup = Dismissibles::get('Test Popup'); 117 | 118 | $showPopup = $popup->shouldBeVisibleTo($user); 119 | ``` 120 | 121 |
122 | 123 | ### 4. Dismiss it for a specified period 124 | ```php 125 | use Rellix\Dismissibles\Facades\Dismissibles; 126 | 127 | Dismissibles::dismiss('Test Popup', $user)->untilNextWeek(); 128 | 129 | // Here's an overview of all the ways you can dismiss: 130 | Dismissibles::dismiss('Test Popup', $user) 131 | ->untilTomorrow(); 132 | ->untilNextWeek(); 133 | ->untilNextMonth(); 134 | ->untilNextQuarter(); 135 | ->untilNextYear(); 136 | ->until($dateTime); 137 | ->forHours($numberOfHours); 138 | ->forDays($numberOfDays); 139 | ->forWeeks($numberOfWeeks); 140 | ->forMonths($numberOfMonths); 141 | ->forYears($numberOfYears); 142 | ->forever(); 143 | ``` 144 | 145 |
146 | 147 | 💡 You can also use the individual models. 148 | 149 | ```php 150 | use Rellix\Dismissibles\Facades\Dismissibles; 151 | 152 | $popup = Dismissibles::get('Test Popup'); 153 | 154 | // Here's an overview of all the ways you can dismiss: 155 | $popup->dismissFor($user) 156 | ->untilTomorrow(); 157 | ->untilNextWeek(); 158 | ->untilNextMonth(); 159 | ->untilNextQuarter(); 160 | ->untilNextYear(); 161 | ->until($dateTime); 162 | ->forHours($numberOfHours); 163 | ->forDays($numberOfDays); 164 | ->forWeeks($numberOfWeeks); 165 | ->forMonths($numberOfMonths); 166 | ->forYears($numberOfYears); 167 | ->forever(); 168 | ``` 169 | 170 |
171 | 172 | ## ❗ Good to know 173 | - The facade contains some oneliners by `$name`, but you can also use the scopes/methods in the `Dismissible` and `Dismissal` Eloquent models as you wish for ultimate flexibility. 174 | - It's recommended to centralize dismissible names in an enum (or config) 175 | - Need extra data regarding the dismissal? All dismiss methods allow you to pass an `$extraData` array as last parameter which will be written to the `dismissals` table as json. 176 | - Feel free to request more methods/scopes 177 | 178 | ## 💾 Database tables 179 | The database structure allows you to easily track activity regarding dismissibles. Due to the `extra_data` column it's also very flexible! 180 | 181 | ### dismissibles (popups, notifications, modals) 182 | | id | name | active_from | active_until | created_at | updated_at | 183 | |----|------------|---------------------|--------------|---------------------|---------------------| 184 | | 3 | Test Popup | 2024-03-01 00:00:00 | null | 2023-12-15 17:35:54 | 2023-12-15 17:35:54 | 185 | 186 | 187 | ### dismissals (activity) 188 | | id | dismissible_id | dismisser_type | dismisser_id | dismissed_until | extra_data | created_at | updated_at | 189 | |----|----------------|-----------------|--------------|---------------------|------------------------------|---------------------|---------------------| 190 | | 15 | 3 | App\Models\User | 328 | 2024-04-29 00:00:00 | "{\"route\":\"home.index\"}" | 2024-04-28 17:35:54 | 2024-04-28 17:35:54 | 191 | 192 | ## ☕ Buy me a coffee 193 | If you like this package, consider [buying me a coffee](https://www.paypal.com/donate/?business=E6QBKXWLXMD92&no_recurring=1&item_name=Buy+me+a+coffee¤cy_code=EUR) :-). 194 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rellix/dismissibles-for-laravel", 3 | "description": "A Laravel package for easily handling the visibility of dismissible, recurring objects like popups/notifications/modals on the server side.", 4 | "type": "library", 5 | "license": "MIT", 6 | "keywords": [ 7 | "laravel", 8 | "dismissibles", 9 | "dismissible", 10 | "dismiss", 11 | "popup", 12 | "notification", 13 | "modal", 14 | "recurring", 15 | "serverside" 16 | ], 17 | "scripts": { 18 | "test": "./vendor/bin/phpunit", 19 | "test-coverage": "./vendor/bin/phpunit --coverage-text", 20 | "phpstan": "./vendor/bin/phpstan analyse -c phpstan.neon" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "Rellix\\Dismissibles\\": "src/", 25 | "Rellix\\Dismissibles\\Database\\Factories\\": "database/factories" 26 | } 27 | }, 28 | "autoload-dev": { 29 | "psr-4": { 30 | "Rellix\\Dismissibles\\Tests\\": "tests" 31 | } 32 | }, 33 | "minimum-stability": "dev", 34 | "prefer-stable": true, 35 | "require": { 36 | "php": "^8.1", 37 | "laravel/framework": ">=7.0" 38 | }, 39 | "extra": { 40 | "laravel": { 41 | "providers": [ 42 | "Rellix\\Dismissibles\\DismissiblesServiceProvider" 43 | ], 44 | "aliases": { 45 | "Dismissibles": "Rellix\\Dismissibles\\Facades\\Dismissibles" 46 | } 47 | } 48 | }, 49 | "require-dev": { 50 | "friendsofphp/php-cs-fixer": "^3.54", 51 | "orchestra/testbench": "^9.0", 52 | "phpstan/phpstan": "^1.10" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /database/factories/DismissalFactory.php: -------------------------------------------------------------------------------- 1 | fn () => TestDismisserTypeOne::factory()->create(), 20 | 'dismisser_type' => TestDismisserTypeOne::class, 21 | 'dismissible_id' => fn () => Dismissible::factory()->create(), 22 | 'dismissed_until' => function (array $attributes) { 23 | if ($this->faker->optional()) { 24 | return null; 25 | } 26 | 27 | $dismissible = Dismissible::find($attributes['dismissible_id']); 28 | 29 | return $this->faker->dateTimeBetween($dismissible->active_from, $dismissible->active_until); 30 | }, 31 | 'extra_data' => $this->faker->optional() ? ['some_extra_data' => $this->faker->randomNumber()] : null, 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /database/factories/DismissibleFactory.php: -------------------------------------------------------------------------------- 1 | faker->dateTimeBetween('-3 weeks', 'now'); 18 | 19 | return [ 20 | 'name' => $this->faker->unique()->text(), 21 | 'active_from' => $activeFrom, 22 | 'active_until' => $this->faker->optional() ? $this->faker->dateTimeBetween($activeFrom, '+3 weeks') : null, 23 | ]; 24 | } 25 | 26 | public function active(?CarbonPeriod $period = null): Factory 27 | { 28 | if ($period === null) { 29 | $activeFrom = $this->faker->dateTimeBetween('-4 weeks', '-1 week'); 30 | $activeUntil = $this->faker->optional() ? $this->faker->dateTimeBetween('+1 week', '+4 weeks') : null; 31 | 32 | $period = CarbonPeriod::create($activeFrom, $activeUntil); 33 | } 34 | 35 | return $this->state(function (array $attributes) use ($period) { 36 | return [ 37 | 'active_from' => $period->start, 38 | 'active_until' => $period->end, 39 | ]; 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /database/factories/TestDismisserTypeOneFactory.php: -------------------------------------------------------------------------------- 1 | id(); 14 | $table->string('name')->unique(); 15 | $table->dateTime('active_from'); 16 | $table->dateTime('active_until')->nullable(); 17 | $table->timestamps(); 18 | }); 19 | 20 | Schema::create('dismissals', function (Blueprint $table) { 21 | $table->id(); 22 | $table->unsignedBigInteger('dismissible_id'); 23 | $table->morphs('dismisser'); 24 | $table->dateTime('dismissed_until')->nullable(); 25 | $table->json('extra_data')->nullable(); 26 | $table->timestamps(); 27 | 28 | $table 29 | ->foreign('dismissible_id') 30 | ->references('id') 31 | ->on('dismissibles') 32 | ->cascadeOnUpdate() 33 | ->cascadeOnDelete(); 34 | 35 | $table->unique( 36 | ['dismissible_id', 'dismisser_type', 'dismisser_id', 'dismissed_until'], 37 | 'dismisser_dismissible_until_unique' 38 | ); 39 | }); 40 | } 41 | 42 | public function down(): void 43 | { 44 | Schema::dropIfExists('dismissibles'); 45 | Schema::dropIfExists('dismissals'); 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /images/dismissibles-for-laravel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rellix999/dismissibles-for-laravel/bfa1278a0a4cb8357b9a07fd15b0577d8f1725ed/images/dismissibles-for-laravel.jpg -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 9 3 | paths: 4 | - src 5 | ignoreErrors: 6 | # Since $extraData comes from the library consumer, we don't know the structure/contents: 7 | - 8 | message: '#Method [a-zA-Z0-9\\:()]+ has parameter \$extraData with no value type specified in iterable type array#' 9 | path: src/Concerns/Dismiss.php 10 | # Laravel magic: 11 | - message: '#Call to an undefined static method Rellix\\Dismissibles\\Models\\Dismissible::active\(\).#' 12 | - message: '#Call to an undefined static method Rellix\\Dismissibles\\Models\\Dismissible::firstWhere\(\).#' 13 | - message: '#Call to an undefined static method Rellix\\Dismissibles\\Models\\Dismissible::visibleTo\(\).#' 14 | - message: '#Call to an undefined method Illuminate\\Database\\Eloquent\\Relations\\HasMany::dismissedBy\(\).#' 15 | - message: '#Call to an undefined method Illuminate\\Database\\Eloquent\\Builder::dismissedBy\(\).#' 16 | - message: '#Call to an undefined method Illuminate\\Database\\Eloquent\\Builder::active\(\).#' 17 | - message: '#Access to an undefined property Rellix\\Dismissibles\\Contracts\\Dismisser::\$id#' 18 | - message: '#Parameter \#[0-9] of method Illuminate\\Database\\Eloquent\\Relations\\MorphTo::associate() expects Illuminate\\Database\\Eloquent\\Model|null, Rellix\\Dismissibles\\Contracts\\Dismisser given#' 19 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests 15 | 16 | 17 | 18 | 19 | 20 | ./src 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Concerns/Dismiss.php: -------------------------------------------------------------------------------- 1 | $dismissibles */ 17 | private function __construct( 18 | public readonly Dismisser $dismisser, 19 | public readonly Collection $dismissibles 20 | ) { 21 | } 22 | 23 | public static function single(Dismisser $dismisser, Dismissible $dismissible): self 24 | { 25 | return new self($dismisser, new Collection([$dismissible])); 26 | } 27 | 28 | /** @param Collection $dismissibles */ 29 | public static function multiple(Dismisser $dismisser, Collection $dismissibles): self 30 | { 31 | return new self($dismisser, $dismissibles); 32 | } 33 | 34 | public function untilTomorrow(?array $extraData = null): void 35 | { 36 | $until = Carbon::tomorrow(); 37 | 38 | $this->dismiss($until, $extraData); 39 | } 40 | 41 | public function untilNextWeek(?array $extraData = null): void 42 | { 43 | $until = Carbon::now()->addWeek()->startOfWeek(); 44 | 45 | $this->dismiss($until, $extraData); 46 | } 47 | 48 | public function untilNextMonth(?array $extraData = null): void 49 | { 50 | $until = Carbon::now()->addMonth()->startOfMonth(); 51 | 52 | $this->dismiss($until, $extraData); 53 | } 54 | 55 | public function untilNextQuarter(?array $extraData = null): void 56 | { 57 | $until = Carbon::now()->addQuarter()->startOfQuarter(); 58 | 59 | $this->dismiss($until, $extraData); 60 | } 61 | 62 | public function untilNextYear(?array $extraData = null): void 63 | { 64 | $until = Carbon::now()->addYear()->startOfYear(); 65 | 66 | $this->dismiss($until, $extraData); 67 | } 68 | 69 | public function until(DateTimeInterface $dateTime, ?array $extraData = null): void 70 | { 71 | $this->dismiss($dateTime, $extraData); 72 | } 73 | 74 | public function forHours(int $hours, ?array $extraData = null): void 75 | { 76 | $until = Carbon::now()->addHours($hours); 77 | 78 | $this->dismiss($until, $extraData); 79 | } 80 | 81 | public function forDays(int $days, ?array $extraData = null): void 82 | { 83 | $until = Carbon::now()->addDays($days); 84 | 85 | $this->dismiss($until, $extraData); 86 | } 87 | 88 | public function forWeeks(int $weeks, ?array $extraData = null): void 89 | { 90 | $until = Carbon::now()->addWeeks($weeks); 91 | 92 | $this->dismiss($until, $extraData); 93 | } 94 | 95 | public function forMonths(int $months, ?array $extraData = null): void 96 | { 97 | $until = Carbon::now()->addMonths($months); 98 | 99 | $this->dismiss($until, $extraData); 100 | } 101 | 102 | public function forYears(int $years, ?array $extraData = null): void 103 | { 104 | $until = Carbon::now()->addYears($years); 105 | 106 | $this->dismiss($until, $extraData); 107 | } 108 | 109 | public function forever(?array $extraData = null): void 110 | { 111 | $this->dismiss(null, $extraData); 112 | } 113 | 114 | private function dismiss(?DateTimeInterface $until = null, ?array $extraData = null): void 115 | { 116 | foreach ($this->dismissibles as $dismissible) { 117 | $dismissal = new Dismissal([ 118 | 'dismissed_until' => $until, 119 | 'extra_data' => $extraData ? json_encode($extraData) : null, 120 | ]); 121 | 122 | $dismissal->dismisser()->associate($this->dismisser); 123 | $dismissal->dismissible()->associate($dismissible); 124 | 125 | $dismissal->save(); 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/Contracts/Dismisser.php: -------------------------------------------------------------------------------- 1 | loadMigrationsFrom(__DIR__ . '/../database/migrations'); 16 | 17 | if (App::environment() === 'testing') { 18 | $this->loadMigrationsFrom(__DIR__ . '/../tests/database/migrations'); 19 | } 20 | } 21 | 22 | public function register(): void 23 | { 24 | $this->app->bind('dismissibles', function ($app) { 25 | return new Dismissibles(); 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Facades/Dismissibles.php: -------------------------------------------------------------------------------- 1 | firstWhere('name', $name); 26 | } 27 | 28 | /** 29 | * Returns all active dismissibles that should be visible to the $dismisser. 30 | * 31 | * @return Collection 32 | */ 33 | public static function getAllFor(Dismisser $dismisser): Collection 34 | { 35 | return Dismissible::visibleTo($dismisser)->orderBy('active_from', 'asc')->get(); 36 | } 37 | 38 | /** 39 | * Returns whether the dismissible should be visible to the dismisser at the current moment. 40 | */ 41 | public static function shouldBeVisible(string $name, Dismisser $dismisser): bool 42 | { 43 | $dismissible = self::get($name); 44 | if (!$dismissible) { 45 | return false; 46 | } 47 | 48 | return $dismissible->shouldBeVisibleTo($dismisser); 49 | } 50 | 51 | /** 52 | * Returns a Dismiss object which allows you to dismiss the dismissible for a specified period. 53 | */ 54 | public static function dismiss(string $name, Dismisser $dismisser): ?Dismiss 55 | { 56 | $dismissible = self::get($name); 57 | if (!$dismissible) { 58 | return null; 59 | } 60 | 61 | return $dismissible->dismissFor($dismisser); 62 | } 63 | 64 | /** 65 | * Returns whether a dismissible is currently dismissed by the dismisser. 66 | */ 67 | public static function isDismissed(string $name, Dismisser $dismisser): bool 68 | { 69 | /** @var Dismissible $dismissible */ 70 | $dismissible = Dismissible::firstWhere('name', $name); 71 | 72 | return $dismissible->isDismissedBy($dismisser); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Models/Dismissal.php: -------------------------------------------------------------------------------- 1 | */ 26 | protected $casts = [ 27 | 'dismissed_until' => 'immutable_datetime', 28 | 'extra_data' => 'array', 29 | ]; 30 | 31 | protected static function newFactory(): DismissalFactory 32 | { 33 | return DismissalFactory::new(); 34 | } 35 | 36 | public function dismissible(): BelongsTo 37 | { 38 | return $this->belongsTo(Dismissible::class); 39 | } 40 | 41 | public function dismisser(): MorphTo 42 | { 43 | return $this->morphTo(); 44 | } 45 | 46 | public function scopeDismissedBy(Builder $query, Dismisser $dismisser): void 47 | { 48 | $query 49 | ->where('dismisser_type', get_class($dismisser)) 50 | ->where('dismisser_id', $dismisser->id); 51 | } 52 | 53 | public function scopeDismissedAt(Builder $query, ?Carbon $moment = null): void 54 | { 55 | if (!$moment) { 56 | $moment = Carbon::now(); 57 | } 58 | 59 | $query 60 | ->where('dismissed_until', '>', $moment) 61 | ->orWhereNull('dismissed_until'); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Models/Dismissible.php: -------------------------------------------------------------------------------- 1 | */ 32 | protected $casts = [ 33 | 'active_from' => 'immutable_datetime', 34 | 'active_until' => 'immutable_datetime', 35 | ]; 36 | 37 | protected static function newFactory(): DismissibleFactory 38 | { 39 | return DismissibleFactory::new(); 40 | } 41 | 42 | public function dismissals(): HasMany 43 | { 44 | return $this->hasMany(Dismissal::class); 45 | } 46 | 47 | public function activePeriod(): CarbonPeriod 48 | { 49 | return CarbonPeriod::create($this->active_from, $this->active_until); 50 | } 51 | 52 | public function shouldBeVisibleTo(Dismisser $dismisser): bool 53 | { 54 | return !$this->isDismissedBy($dismisser); 55 | } 56 | 57 | public function dismissFor(Dismisser $dismisser): Dismiss 58 | { 59 | return Dismiss::single($dismisser, $this); 60 | } 61 | 62 | public function isDismissedBy(Dismisser $dismisser, ?Carbon $moment = null): bool 63 | { 64 | if (!$moment) { 65 | $moment = Carbon::now(); 66 | } 67 | 68 | return $this->dismissals() 69 | ->dismissedBy($dismisser) 70 | ->dismissedAt($moment) 71 | ->exists(); 72 | } 73 | 74 | public function scopeVisibleTo(Builder $query, Dismisser $dismisser): void 75 | { 76 | $query->active()->notDismissedBy($dismisser); 77 | } 78 | 79 | public function scopeActive(Builder $query, ?Carbon $moment = null): void 80 | { 81 | if (!$moment) { 82 | $moment = Carbon::now(); 83 | } 84 | 85 | $query 86 | ->where('active_from', '<=', $moment) 87 | ->where(function (Builder $query) use ($moment) { 88 | $query 89 | ->where('active_until', '>', $moment) 90 | ->orWhereNull('active_until'); 91 | }); 92 | } 93 | 94 | public function scopeNotDismissedBy(Builder $query, Dismisser $dismisser, ?Carbon $moment = null): void 95 | { 96 | if (!$moment) { 97 | $moment = Carbon::now(); 98 | } 99 | 100 | $query->whereDoesntHave('dismissals', function (Builder $query) use ($dismisser, $moment) { 101 | $query->dismissedBy($dismisser)->dismissedAt($moment); 102 | }); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Models/TestDismisserTypeOne.php: -------------------------------------------------------------------------------- 1 | morphMany(Dismissal::class, 'dismisser'); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/BaseTestCase.php: -------------------------------------------------------------------------------- 1 | artisan('migrate', ['--database' => 'test'])->run(); 17 | } 18 | 19 | protected function getPackageProviders($app) 20 | { 21 | return [DismissiblesServiceProvider::class]; 22 | } 23 | 24 | protected function getEnvironmentSetUp($app) 25 | { 26 | $app['config']->set('database.default', 'test'); 27 | 28 | $app['config']->set('database.connections.test', [ 29 | 'driver' => 'sqlite', 30 | 'database' => ':memory:', 31 | 'prefix' => '', 32 | ]); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Unit/Concerns/DismissTest.php: -------------------------------------------------------------------------------- 1 | dismissible = Dismissible::factory()->create(); 31 | $this->dismisser = TestDismisserTypeOne::factory()->create(); 32 | $this->dismiss = Dismiss::single($this->dismisser, $this->dismissible); 33 | } 34 | 35 | #[Test] 36 | public function it_creates_a_dismissal_with_the_correct_type_and_id_for_type_one() 37 | { 38 | $dismissible = Dismissible::factory()->create(); 39 | $dismisser = TestDismisserTypeOne::factory()->create(); 40 | $dismiss = Dismiss::single($dismisser, $dismissible); 41 | 42 | $dismiss->untilTomorrow(); 43 | 44 | /** @var Dismissal $dismissal */ 45 | $dismissal = Dismissal::orderBy('id', 'desc')->first(); 46 | 47 | $this->assertDatabaseHas('dismissals', [ 48 | 'id' => $dismissal->id, 49 | 'dismisser_type' => TestDismisserTypeOne::class, 50 | 'dismisser_id' => $dismisser->id, 51 | ]); 52 | } 53 | 54 | #[Test] 55 | public function it_creates_a_dismissal_with_the_correct_type_and_id_for_type_two() 56 | { 57 | $dismissible = Dismissible::factory()->create(); 58 | $dismisser = TestDismisserTypeTwo::factory()->create(); 59 | $dismiss = Dismiss::single($dismisser, $dismissible); 60 | 61 | $dismiss->untilTomorrow(); 62 | 63 | /** @var Dismissal $dismissal */ 64 | $dismissal = Dismissal::orderBy('id', 'desc')->first(); 65 | 66 | $this->assertDatabaseHas('dismissals', [ 67 | 'id' => $dismissal->id, 68 | 'dismisser_type' => TestDismisserTypeTwo::class, 69 | 'dismisser_id' => $dismisser->id, 70 | ]); 71 | } 72 | 73 | #[Test] 74 | #[DataProvider('untilTomorrowDataProvider')] 75 | public function until_tomorrow(string $now, string $expectedDismissedUntil) 76 | { 77 | $this->setTestNow($now); 78 | 79 | $this->dismiss->untilTomorrow(); 80 | 81 | $expectedData = $this->getExpectedDismissalData([ 82 | 'dismissed_until' => $expectedDismissedUntil, 83 | ]); 84 | 85 | $this->assertDatabaseHas('dismissals', $expectedData); 86 | } 87 | 88 | #[Test] 89 | #[DataProvider('untilNextWeekDataProvider')] 90 | public function until_next_week(string $now, string $expectedDismissedUntil) 91 | { 92 | $this->setTestNow($now); 93 | 94 | $this->dismiss->untilNextWeek(); 95 | 96 | $expectedData = $this->getExpectedDismissalData([ 97 | 'dismissed_until' => $expectedDismissedUntil, 98 | ]); 99 | 100 | $this->assertDatabaseHas('dismissals', $expectedData); 101 | } 102 | 103 | #[Test] 104 | #[DataProvider('untilNextMonthDataProvider')] 105 | public function until_next_month(string $now, string $expectedDismissedUntil) 106 | { 107 | $this->setTestNow($now); 108 | 109 | $this->dismiss->untilNextMonth(); 110 | 111 | $expectedData = $this->getExpectedDismissalData([ 112 | 'dismissed_until' => $expectedDismissedUntil, 113 | ]); 114 | 115 | $this->assertDatabaseHas('dismissals', $expectedData); 116 | } 117 | 118 | #[Test] 119 | #[DataProvider('untilNextQuarterDataProvider')] 120 | public function until_next_quarter(string $now, string $expectedDismissedUntil) 121 | { 122 | $this->setTestNow($now); 123 | 124 | $this->dismiss->untilNextQuarter(); 125 | 126 | $expectedData = $this->getExpectedDismissalData([ 127 | 'dismissed_until' => $expectedDismissedUntil, 128 | ]); 129 | 130 | $this->assertDatabaseHas('dismissals', $expectedData); 131 | } 132 | 133 | #[Test] 134 | #[DataProvider('untilNextYearDataProvider')] 135 | public function until_next_year(string $now, string $expectedDismissedUntil) 136 | { 137 | $this->setTestNow($now); 138 | 139 | $this->dismiss->untilNextYear(); 140 | 141 | $expectedData = $this->getExpectedDismissalData([ 142 | 'dismissed_until' => $expectedDismissedUntil, 143 | ]); 144 | 145 | $this->assertDatabaseHas('dismissals', $expectedData); 146 | } 147 | 148 | #[Test] 149 | #[DataProvider('untilDataProvider')] 150 | public function until(string $now, DateTimeInterface $dateTime, string $expectedDismissedUntil) 151 | { 152 | $this->setTestNow($now); 153 | 154 | $this->dismiss->until($dateTime); 155 | 156 | $expectedData = $this->getExpectedDismissalData([ 157 | 'dismissed_until' => $expectedDismissedUntil, 158 | ]); 159 | 160 | $this->assertDatabaseHas('dismissals', $expectedData); 161 | } 162 | 163 | #[Test] 164 | #[DataProvider('forHoursDataProvider')] 165 | public function for_hours(string $now, int $hours, string $expectedDismissedUntil) 166 | { 167 | $this->setTestNow($now); 168 | 169 | $this->dismiss->forHours($hours); 170 | 171 | $expectedData = $this->getExpectedDismissalData([ 172 | 'dismissed_until' => $expectedDismissedUntil, 173 | ]); 174 | 175 | $this->assertDatabaseHas('dismissals', $expectedData); 176 | } 177 | 178 | #[Test] 179 | #[DataProvider('forDaysDataProvider')] 180 | public function for_days(string $now, int $days, string $expectedDismissedUntil) 181 | { 182 | $this->setTestNow($now); 183 | 184 | $this->dismiss->forDays($days); 185 | 186 | $expectedData = $this->getExpectedDismissalData([ 187 | 'dismissed_until' => $expectedDismissedUntil, 188 | ]); 189 | 190 | $this->assertDatabaseHas('dismissals', $expectedData); 191 | } 192 | 193 | #[Test] 194 | #[DataProvider('forWeeksDataProvider')] 195 | public function for_weeks(string $now, int $weeks, string $expectedDismissedUntil) 196 | { 197 | $this->setTestNow($now); 198 | 199 | $this->dismiss->forWeeks($weeks); 200 | 201 | $expectedData = $this->getExpectedDismissalData([ 202 | 'dismissed_until' => $expectedDismissedUntil, 203 | ]); 204 | 205 | $this->assertDatabaseHas('dismissals', $expectedData); 206 | } 207 | 208 | #[Test] 209 | #[DataProvider('forMonthsDataProvider')] 210 | public function for_months(string $now, int $months, string $expectedDismissedUntil) 211 | { 212 | $this->setTestNow($now); 213 | 214 | $this->dismiss->forMonths($months); 215 | 216 | $expectedData = $this->getExpectedDismissalData([ 217 | 'dismissed_until' => $expectedDismissedUntil, 218 | ]); 219 | 220 | $this->assertDatabaseHas('dismissals', $expectedData); 221 | } 222 | 223 | #[Test] 224 | #[DataProvider('forYearsDataProvider')] 225 | public function for_years(string $now, int $years, string $expectedDismissedUntil) 226 | { 227 | $this->setTestNow($now); 228 | 229 | $this->dismiss->forYears($years); 230 | 231 | $expectedData = $this->getExpectedDismissalData([ 232 | 'dismissed_until' => $expectedDismissedUntil, 233 | ]); 234 | 235 | $this->assertDatabaseHas('dismissals', $expectedData); 236 | } 237 | 238 | #[Test] 239 | #[DataProvider('foreverDataProvider')] 240 | public function forever(string $now) 241 | { 242 | $this->setTestNow($now); 243 | 244 | $this->dismiss->forever(); 245 | 246 | $expectedData = $this->getExpectedDismissalData([ 247 | 'dismissed_until' => null, 248 | ]); 249 | 250 | $this->assertDatabaseHas('dismissals', $expectedData); 251 | } 252 | 253 | public static function untilTomorrowDataProvider(): array 254 | { 255 | return [ 256 | ['2023-01-01 00:00:00', '2023-01-02 00:00:00'], 257 | ['2023-08-16 14:00:00', '2023-08-17 00:00:00'], 258 | ['2023-12-31 23:59:59', '2024-01-01 00:00:00'], 259 | ]; 260 | } 261 | 262 | public static function untilNextWeekDataProvider() 263 | { 264 | return [ 265 | ['2023-08-16 14:00:00', '2023-08-21 00:00:00'], 266 | ['2023-09-01 12:00:00', '2023-09-04 00:00:00'], 267 | ]; 268 | } 269 | 270 | public static function untilNextMonthDataProvider() 271 | { 272 | return [ 273 | ['2023-08-16 14:00:00', '2023-09-01 00:00:00'], 274 | ['2023-09-01 13:01:59', '2023-10-01 00:00:00'], 275 | ]; 276 | } 277 | 278 | public static function untilNextQuarterDataProvider() 279 | { 280 | return [ 281 | ['2023-08-16 14:00:00', '2023-10-01 00:00:00'], 282 | ['2023-01-01 22:10:13', '2023-04-01 00:00:00'], 283 | ]; 284 | } 285 | 286 | public static function untilNextYearDataProvider() 287 | { 288 | return [ 289 | ['2023-08-16 14:00:00', '2024-01-01 00:00:00'], 290 | ['2023-01-01 14:00:00', '2024-01-01 00:00:00'], 291 | ['2024-12-31 14:00:00', '2025-01-01 00:00:00'], 292 | ]; 293 | } 294 | 295 | public static function untilDataProvider() 296 | { 297 | return [ 298 | [ 299 | '2023-08-16 14:00:00', 300 | Carbon::createFromFormat('d-m-Y H:i:s', '20-08-2023 12:32:02'), 301 | '2023-08-20 12:32:02', 302 | ], 303 | [ 304 | '2023-01-01 14:00:00', 305 | Carbon::createFromFormat('d-m-Y H:i:s', '01-01-2023 14:00:01'), 306 | '2023-01-01 14:00:01', 307 | ], 308 | ]; 309 | } 310 | 311 | public static function forHoursDataProvider(): array 312 | { 313 | return [ 314 | ['2023-08-16 14:00:00', 1, '2023-08-16 15:00:00'], 315 | ['2023-08-16 14:00:00', 2, '2023-08-16 16:00:00'], 316 | ['2023-08-16 14:00:00', 10, '2023-08-17 00:00:00'], 317 | ['2023-08-16 14:00:00', 24, '2023-08-17 14:00:00'], 318 | ['2023-08-16 14:00:00', 48, '2023-08-18 14:00:00'], 319 | ['2023-08-16 14:00:00', 100, '2023-08-20 18:00:00'], 320 | ]; 321 | } 322 | 323 | public static function forDaysDataProvider(): array 324 | { 325 | return [ 326 | ['2023-08-16 14:00:00', 1, '2023-08-17 14:00:00'], 327 | ['2023-08-16 14:00:00', 2, '2023-08-18 14:00:00'], 328 | ['2023-08-16 14:00:00', 7, '2023-08-23 14:00:00'], 329 | ['2023-08-16 14:00:00', 100, '2023-11-24 14:00:00'], 330 | ['2023-08-16 14:00:00', 365, '2024-08-15 14:00:00'], 331 | ]; 332 | } 333 | 334 | public static function forWeeksDataProvider(): array 335 | { 336 | return [ 337 | ['2023-08-16 14:00:00', 1, '2023-08-23 14:00:00'], 338 | ['2023-08-16 14:00:00', 2, '2023-08-30 14:00:00'], 339 | ['2023-08-16 14:00:00', 7, '2023-10-04 14:00:00'], 340 | ['2023-08-16 14:00:00', 52, '2024-08-14 14:00:00'], 341 | ]; 342 | } 343 | 344 | public static function forMonthsDataProvider(): array 345 | { 346 | return [ 347 | ['2023-08-16 14:00:00', 1, '2023-09-16 14:00:00'], 348 | ['2023-08-16 14:00:00', 2, '2023-10-16 14:00:00'], 349 | ['2023-08-16 14:00:00', 6, '2024-02-16 14:00:00'], 350 | ['2023-08-16 14:00:00', 12, '2024-08-16 14:00:00'], 351 | ]; 352 | } 353 | 354 | public static function forYearsDataProvider(): array 355 | { 356 | return [ 357 | ['2023-08-16 14:00:00', 1, '2024-08-16 14:00:00'], 358 | ['2023-08-16 14:00:00', 2, '2025-08-16 14:00:00'], 359 | ['2023-08-16 14:00:00', 6, '2029-08-16 14:00:00'], 360 | ['2023-08-16 14:00:00', 12, '2035-08-16 14:00:00'], 361 | ['2023-08-16 14:00:00', 50, '2073-08-16 14:00:00'], 362 | ]; 363 | } 364 | 365 | public static function foreverDataProvider() 366 | { 367 | return [ 368 | ['2023-08-16 14:00:00'], 369 | ['2023-08-16 14:00:00'], 370 | ['2023-08-16 14:00:00'], 371 | ]; 372 | } 373 | 374 | private function setTestNow(string $now) 375 | { 376 | Carbon::setTestNow(Carbon::createFromFormat('Y-m-d H:i:s', $now)); 377 | } 378 | 379 | private function getExpectedDismissalData(array $expectedData): array 380 | { 381 | return [ 382 | 'dismissible_id' => $this->dismissible->id, 383 | 'dismisser_id' => $this->dismisser->id, 384 | 'dismisser_type' => TestDismisserTypeOne::class, 385 | ...$expectedData, 386 | ]; 387 | } 388 | } 389 | -------------------------------------------------------------------------------- /tests/Unit/Facades/Dismissibles/DismissTest.php: -------------------------------------------------------------------------------- 1 | dismissible = Dismissible::factory()->active()->create(); 27 | $this->dismisser = TestDismisserTypeOne::factory()->create(); 28 | } 29 | 30 | #[Test] 31 | public function it_returns_null_before_active_period() 32 | { 33 | $now = CarbonImmutable::now(); 34 | 35 | $dismissible = Dismissible::factory() 36 | ->active(CarbonPeriod::create($now->subWeek(), $now->subDay())) 37 | ->create(); 38 | 39 | $actualValue = Dismissibles::dismiss($dismissible->name, $this->dismisser); 40 | 41 | $this->assertNull($actualValue); 42 | } 43 | 44 | #[Test] 45 | public function it_returns_an_object_during_active_period() 46 | { 47 | $actualValue = Dismissibles::dismiss($this->dismissible->name, $this->dismisser); 48 | 49 | $this->assertIsObject($actualValue); 50 | } 51 | 52 | #[Test] 53 | public function it_returns_a_dismiss_object_during_active_period() 54 | { 55 | $actualValue = Dismissibles::dismiss($this->dismissible->name, $this->dismisser); 56 | 57 | $this->assertInstanceOf(Dismiss::class, $actualValue); 58 | } 59 | 60 | #[Test] 61 | public function the_dismiss_object_has_the_correct_dismisser() 62 | { 63 | $actualValue = Dismissibles::dismiss($this->dismissible->name, $this->dismisser); 64 | 65 | $this->assertTrue($actualValue->dismisser->is($this->dismisser)); 66 | } 67 | 68 | #[Test] 69 | public function the_dismiss_object_has_the_correct_dismissible_no_more_no_less() 70 | { 71 | $actualValue = Dismissibles::dismiss($this->dismissible->name, $this->dismisser); 72 | 73 | $dismissibleIds = $actualValue->dismissibles->pluck('id')->toArray(); 74 | 75 | $this->assertEquals([$this->dismissible->id], $dismissibleIds); 76 | } 77 | 78 | #[Test] 79 | public function it_returns_null_after_active_period() 80 | { 81 | $now = CarbonImmutable::now(); 82 | 83 | $dismissible = Dismissible::factory() 84 | ->active(CarbonPeriod::create($now->subWeek(), $now->subDay())) 85 | ->create(); 86 | 87 | $actualValue = Dismissibles::dismiss($dismissible->name, $this->dismisser); 88 | 89 | $this->assertNull($actualValue); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /tests/Unit/Facades/Dismissibles/GetAllForTest.php: -------------------------------------------------------------------------------- 1 | create(); 23 | 24 | $actualResult = Dismissibles::getAllFor($dismisser); 25 | 26 | $this->assertInstanceOf(Collection::class, $actualResult); 27 | $this->assertCount(0, $actualResult); 28 | } 29 | 30 | #[Test] 31 | public function it_returns_no_inactive_dismissibles() 32 | { 33 | $dismisser = TestDismisserTypeOne::factory()->create(); 34 | 35 | $now = CarbonImmutable::now(); 36 | 37 | Dismissible::factory() 38 | ->active(CarbonPeriod::create($now->subWeek(), $now->subDay())) 39 | ->create(); 40 | 41 | Dismissible::factory() 42 | ->active(CarbonPeriod::create($now->addDay(), $now->addWeek())) 43 | ->create(); 44 | 45 | $actualResult = Dismissibles::getAllFor($dismisser); 46 | 47 | $this->assertInstanceOf(Collection::class, $actualResult); 48 | $this->assertCount(0, $actualResult); 49 | } 50 | 51 | #[Test] 52 | public function it_returns_active_dismissibles() 53 | { 54 | $dismisser = TestDismisserTypeOne::factory()->create(); 55 | 56 | $now = CarbonImmutable::now(); 57 | 58 | Dismissible::factory() 59 | ->active(CarbonPeriod::create($now->subWeek(), $now->addWeek())) 60 | ->create(); 61 | 62 | $actualResult = Dismissibles::getAllFor($dismisser); 63 | 64 | $this->assertInstanceOf(Collection::class, $actualResult); 65 | $this->assertCount(1, $actualResult); 66 | } 67 | 68 | #[Test] 69 | public function it_returns_dismissibles_which_are_dismissed_until_past_date_time() 70 | { 71 | $dismisser = TestDismisserTypeOne::factory()->create(); 72 | 73 | $now = CarbonImmutable::now(); 74 | 75 | $dismissible = Dismissible::factory() 76 | ->active(CarbonPeriod::create($now->subWeek(), $now->addWeek())) 77 | ->create(); 78 | 79 | Dismissal::factory() 80 | ->for($dismisser, 'dismisser') 81 | ->for($dismissible) 82 | ->create([ 83 | 'dismissed_until' => $now->subDay(), 84 | ]); 85 | 86 | $actualResult = Dismissibles::getAllFor($dismisser); 87 | 88 | $this->assertInstanceOf(Collection::class, $actualResult); 89 | $this->assertCount(1, $actualResult); 90 | } 91 | 92 | #[Test] 93 | public function it_returns_dismissibles_which_are_dismissed_until_dismissible_active_from_date_time() 94 | { 95 | $dismisser = TestDismisserTypeOne::factory()->create(); 96 | 97 | $now = CarbonImmutable::now(); 98 | 99 | /** @var Dismissible $dismissible */ 100 | $dismissible = Dismissible::factory() 101 | ->active(CarbonPeriod::create($now->subWeek(), $now->addWeek())) 102 | ->create(); 103 | 104 | Dismissal::factory() 105 | ->for($dismisser, 'dismisser') 106 | ->for($dismissible) 107 | ->create([ 108 | 'dismissed_until' => $dismissible->active_from, 109 | ]); 110 | 111 | $actualResult = Dismissibles::getAllFor($dismisser); 112 | 113 | $this->assertInstanceOf(Collection::class, $actualResult); 114 | $this->assertCount(1, $actualResult); 115 | } 116 | 117 | #[Test] 118 | public function it_returns_dismissibles_ordered_by_active_from_ascending() 119 | { 120 | $dismisser = TestDismisserTypeOne::factory()->create(); 121 | 122 | $now = CarbonImmutable::now(); 123 | 124 | /** @var Dismissible $dismissible */ 125 | $newestDismissible = Dismissible::factory() 126 | ->active(CarbonPeriod::create($now->subDay(), $now->addWeek())) 127 | ->create(); 128 | 129 | /** @var Dismissible $dismissible */ 130 | $oldestDismissible = Dismissible::factory() 131 | ->active(CarbonPeriod::create($now->subMonth(), $now->addWeek())) 132 | ->create(); 133 | 134 | /** @var Dismissible $dismissible */ 135 | $middleDismissible = Dismissible::factory() 136 | ->active(CarbonPeriod::create($now->subWeek(), $now->addWeek())) 137 | ->create(); 138 | 139 | $actualResult = Dismissibles::getAllFor($dismisser); 140 | 141 | $this->assertTrue($actualResult->get(0)->is($oldestDismissible)); 142 | $this->assertTrue($actualResult->get(1)->is($middleDismissible)); 143 | $this->assertTrue($actualResult->get(2)->is($newestDismissible)); 144 | } 145 | 146 | #[Test] 147 | public function it_does_not_return_dismissibles_which_are_dismissed_until_future_date_time() 148 | { 149 | $dismisser = TestDismisserTypeOne::factory()->create(); 150 | 151 | $now = CarbonImmutable::now(); 152 | 153 | /** @var Dismissible $dismissible */ 154 | $dismissible = Dismissible::factory() 155 | ->active(CarbonPeriod::create($now->subWeek(), $now->addWeek())) 156 | ->create(); 157 | 158 | Dismissal::factory() 159 | ->for($dismisser, 'dismisser') 160 | ->for($dismissible) 161 | ->create([ 162 | 'dismissed_until' => $now->addMinute(), 163 | ]); 164 | 165 | $actualResult = Dismissibles::getAllFor($dismisser); 166 | 167 | $this->assertInstanceOf(Collection::class, $actualResult); 168 | $this->assertCount(0, $actualResult); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /tests/Unit/Facades/Dismissibles/GetTest.php: -------------------------------------------------------------------------------- 1 | name = 'test'; 23 | } 24 | 25 | #[Test] 26 | public function it_returns_null_when_name_does_not_exist() 27 | { 28 | $actualResult = Dismissibles::get($this->name); 29 | 30 | $this->assertNull($actualResult); 31 | } 32 | 33 | #[Test] 34 | public function it_returns_null_before_active_period() 35 | { 36 | $now = CarbonImmutable::now(); 37 | 38 | Dismissible::factory() 39 | ->active(CarbonPeriod::create($now->addDay(), $now->addWeek())) 40 | ->create([ 41 | 'name' => $this->name, 42 | ]); 43 | 44 | $actualResult = Dismissibles::get($this->name); 45 | 46 | $this->assertNull($actualResult); 47 | } 48 | 49 | #[Test] 50 | public function it_returns_dismissible_during_active_period_when_active_until_is_set() 51 | { 52 | $now = CarbonImmutable::now(); 53 | 54 | $dismissible = Dismissible::factory() 55 | ->active(CarbonPeriod::create($now->subMinute(), $now->addDay())) 56 | ->create([ 57 | 'name' => $this->name, 58 | ]); 59 | 60 | $actualResult = Dismissibles::get($this->name); 61 | 62 | $this->assertTrue($actualResult->is($dismissible)); 63 | } 64 | 65 | #[Test] 66 | public function it_returns_dismissible_during_active_period_when_active_until_is_null() 67 | { 68 | $now = CarbonImmutable::now(); 69 | 70 | $dismissible = Dismissible::factory() 71 | ->active(CarbonPeriod::create($now->subMinute())) 72 | ->create([ 73 | 'name' => $this->name, 74 | ]); 75 | 76 | $actualResult = Dismissibles::get($this->name); 77 | 78 | $this->assertTrue($actualResult->is($dismissible)); 79 | } 80 | 81 | #[Test] 82 | public function it_returns_null_after_active_period() 83 | { 84 | $now = CarbonImmutable::now(); 85 | 86 | Dismissible::factory() 87 | ->active(CarbonPeriod::create($now->subWeek(), $now->subDay())) 88 | ->create([ 89 | 'name' => $this->name, 90 | ]); 91 | 92 | $actualResult = Dismissibles::get($this->name); 93 | 94 | $this->assertNull($actualResult); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /tests/Unit/Facades/Dismissibles/IsDismissedTest.php: -------------------------------------------------------------------------------- 1 | dismisser = TestDismisserTypeOne::factory()->create(); 26 | } 27 | 28 | #[Test] 29 | public function it_returns_false_when_dismisser_has_not_dismissed() 30 | { 31 | /** @var Dismissible $dismissible */ 32 | $dismissible = Dismissible::factory()->create(); 33 | 34 | $actualValue = Dismissibles::isDismissed($dismissible->name, $this->dismisser); 35 | 36 | $this->assertFalse($actualValue); 37 | } 38 | 39 | #[Test] 40 | public function it_returns_false_when_dismisser_has_dismissed_different_dismissible_until_past_date() 41 | { 42 | /** @var Dismissible $dismissible */ 43 | $dismissible = Dismissible::factory()->create(); 44 | 45 | Dismissal::factory() 46 | ->for($this->dismisser, 'dismisser') 47 | ->create([ 48 | 'dismissed_until' => Carbon::yesterday(), 49 | ]); 50 | 51 | $actualValue = Dismissibles::isDismissed($dismissible->name, $this->dismisser); 52 | 53 | $this->assertFalse($actualValue); 54 | } 55 | 56 | #[Test] 57 | public function it_returns_false_when_dismisser_has_dismissed_different_dismissible_until_future_date() 58 | { 59 | Dismissal::factory() 60 | ->for($this->dismisser, 'dismisser') 61 | ->create([ 62 | 'dismissed_until' => Carbon::tomorrow(), 63 | ]); 64 | 65 | /** @var Dismissible $dismissible */ 66 | $dismissible = Dismissible::factory()->create(); 67 | 68 | $actualValue = Dismissibles::isDismissed($dismissible->name, $this->dismisser); 69 | 70 | $this->assertFalse($actualValue); 71 | } 72 | 73 | #[Test] 74 | public function it_returns_false_when_dismisser_has_dismissed_until_past_date() 75 | { 76 | /** @var Dismissible $dismissible */ 77 | $dismissible = Dismissible::factory()->create(); 78 | 79 | Dismissal::factory() 80 | ->for($dismissible) 81 | ->for($this->dismisser, 'dismisser') 82 | ->create([ 83 | 'dismissed_until' => Carbon::now()->subDay(), 84 | ]); 85 | 86 | $actualValue = Dismissibles::isDismissed($dismissible->name, $this->dismisser); 87 | 88 | $this->assertFalse($actualValue); 89 | } 90 | 91 | #[Test] 92 | public function it_returns_true_when_dismisser_has_dismissed_until_future_date() 93 | { 94 | /** @var Dismissible $dismissible */ 95 | $dismissible = Dismissible::factory()->create(); 96 | 97 | Dismissal::factory() 98 | ->for($dismissible) 99 | ->for($this->dismisser, 'dismisser') 100 | ->create([ 101 | 'dismissed_until' => Carbon::now()->addDay(), 102 | ]); 103 | 104 | $actualValue = Dismissibles::isDismissed($dismissible->name, $this->dismisser); 105 | 106 | $this->assertTrue($actualValue); 107 | } 108 | 109 | #[Test] 110 | public function it_returns_true_when_user_has_dismissed_until_null() 111 | { 112 | /** @var Dismissible $dismissible */ 113 | $dismissible = Dismissible::factory()->create(); 114 | 115 | Dismissal::factory() 116 | ->for($dismissible) 117 | ->for($this->dismisser, 'dismisser') 118 | ->create([ 119 | 'dismissed_until' => null, 120 | ]); 121 | 122 | $actualValue = Dismissibles::isDismissed($dismissible->name, $this->dismisser); 123 | 124 | $this->assertTrue($actualValue); 125 | } 126 | 127 | #[Test] 128 | public function it_returns_the_correct_value_when_dismissal_with_same_id_but_different_type_exists() 129 | { 130 | TestDismisserTypeOne::truncate(); 131 | TestDismisserTypeTwo::truncate(); 132 | 133 | $dismisserOfTypeOne = TestDismisserTypeOne::factory()->create(); 134 | $dismisserOfTypeTwo = TestDismisserTypeTwo::factory()->create(); 135 | 136 | $this->assertEquals($dismisserOfTypeOne->id, $dismisserOfTypeTwo->id); 137 | 138 | /** @var Dismissible $dismissible */ 139 | $dismissible = Dismissible::factory()->active()->create(); 140 | 141 | Dismissal::factory() 142 | ->for($dismissible) 143 | ->for($dismisserOfTypeOne, 'dismisser') 144 | ->create([ 145 | 'dismissed_until' => Carbon::tomorrow(), 146 | ]); 147 | 148 | $this->assertTrue(Dismissibles::isDismissed($dismissible->name, $dismisserOfTypeOne)); 149 | $this->assertFalse(Dismissibles::isDismissed($dismissible->name, $dismisserOfTypeTwo)); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /tests/Unit/Facades/Dismissibles/ShouldShowTest.php: -------------------------------------------------------------------------------- 1 | dismisser = TestDismisserTypeOne::factory()->create(); 26 | } 27 | 28 | #[Test] 29 | public function it_returns_false_when_dismissible_name_does_not_exist() 30 | { 31 | $actualValue = Dismissibles::shouldBeVisible('test', $this->dismisser); 32 | 33 | $this->assertFalse($actualValue); 34 | } 35 | 36 | #[Test] 37 | public function it_returns_false_before_active_period_not_dismissed() 38 | { 39 | $now = CarbonImmutable::now(); 40 | 41 | $dismissible = Dismissible::factory() 42 | ->active(CarbonPeriod::create($now->addDay(), $now->addWeek())) 43 | ->create(); 44 | 45 | $actualValue = Dismissibles::shouldBeVisible($dismissible->name, $this->dismisser); 46 | 47 | $this->assertFalse($actualValue); 48 | } 49 | 50 | #[Test] 51 | public function it_returns_false_before_active_period_dismissed_in_past() 52 | { 53 | $now = CarbonImmutable::now(); 54 | 55 | /** @var Dismissible $dismissible */ 56 | $dismissible = Dismissible::factory() 57 | ->active(CarbonPeriod::create($now->addWeek(), $now->addWeeks(2))) 58 | ->create(); 59 | 60 | Dismissal::factory() 61 | ->for($dismissible) 62 | ->for($this->dismisser, 'dismisser') 63 | ->create([ 64 | 'dismissed_until' => $now->subDay(), 65 | ]); 66 | 67 | $actualValue = Dismissibles::shouldBeVisible($dismissible->name, $this->dismisser); 68 | 69 | $this->assertFalse($actualValue); 70 | } 71 | 72 | #[Test] 73 | public function it_returns_false_before_active_period_dismissed_in_future() 74 | { 75 | $now = CarbonImmutable::now(); 76 | 77 | /** @var Dismissible $dismissible */ 78 | $dismissible = Dismissible::factory() 79 | ->active(CarbonPeriod::create($now->addWeek(), $now->addWeeks(2))) 80 | ->create(); 81 | 82 | Dismissal::factory() 83 | ->for($dismissible) 84 | ->for($this->dismisser, 'dismisser') 85 | ->create([ 86 | 'dismissed_until' => $now->addDay(), 87 | ]); 88 | 89 | $actualValue = Dismissibles::shouldBeVisible($dismissible->name, $this->dismisser); 90 | 91 | $this->assertFalse($actualValue); 92 | } 93 | 94 | #[Test] 95 | public function it_returns_false_before_active_period_dismissed_forever() 96 | { 97 | $now = CarbonImmutable::now(); 98 | 99 | /** @var Dismissible $dismissible */ 100 | $dismissible = Dismissible::factory() 101 | ->active(CarbonPeriod::create($now->addWeek(), $now->addWeeks(2))) 102 | ->create(); 103 | 104 | Dismissal::factory() 105 | ->for($dismissible) 106 | ->for($this->dismisser, 'dismisser') 107 | ->create([ 108 | 'dismissed_until' => null, 109 | ]); 110 | 111 | $actualValue = Dismissibles::shouldBeVisible($dismissible->name, $this->dismisser); 112 | 113 | $this->assertFalse($actualValue); 114 | } 115 | 116 | #[Test] 117 | public function it_returns_true_on_active_period_start_not_dismissed() 118 | { 119 | $now = CarbonImmutable::now(); 120 | 121 | /** @var Dismissible $dismissible */ 122 | $dismissible = Dismissible::factory() 123 | ->active(CarbonPeriod::create($now, $now->addWeeks(2))) 124 | ->create(); 125 | 126 | $actualValue = Dismissibles::shouldBeVisible($dismissible->name, $this->dismisser); 127 | 128 | $this->assertTrue($actualValue); 129 | } 130 | 131 | #[Test] 132 | public function it_returns_true_on_active_period_start_dismissed_in_past() 133 | { 134 | $now = CarbonImmutable::now(); 135 | 136 | /** @var Dismissible $dismissible */ 137 | $dismissible = Dismissible::factory() 138 | ->active(CarbonPeriod::create($now, $now->addWeeks(2))) 139 | ->create(); 140 | 141 | Dismissal::factory() 142 | ->for($dismissible) 143 | ->for($this->dismisser, 'dismisser') 144 | ->create([ 145 | 'dismissed_until' => $now->subDay(), 146 | ]); 147 | 148 | $actualValue = Dismissibles::shouldBeVisible($dismissible->name, $this->dismisser); 149 | 150 | $this->assertTrue($actualValue); 151 | } 152 | 153 | #[Test] 154 | public function it_returns_false_on_active_period_start_dismissed_in_future() 155 | { 156 | $now = CarbonImmutable::now(); 157 | 158 | /** @var Dismissible $dismissible */ 159 | $dismissible = Dismissible::factory() 160 | ->active(CarbonPeriod::create($now, $now->addWeeks(2))) 161 | ->create(); 162 | 163 | Dismissal::factory() 164 | ->for($dismissible) 165 | ->for($this->dismisser, 'dismisser') 166 | ->create([ 167 | 'dismissed_until' => $now->addDay(), 168 | ]); 169 | 170 | $actualValue = Dismissibles::shouldBeVisible($dismissible->name, $this->dismisser); 171 | 172 | $this->assertFalse($actualValue); 173 | } 174 | 175 | #[Test] 176 | public function it_returns_true_during_active_period_not_dismissed() 177 | { 178 | $now = CarbonImmutable::now(); 179 | 180 | /** @var Dismissible $dismissible */ 181 | $dismissible = Dismissible::factory() 182 | ->active(CarbonPeriod::create($now->subDay(), $now->addDay())) 183 | ->create(); 184 | 185 | $actualValue = Dismissibles::shouldBeVisible($dismissible->name, $this->dismisser); 186 | 187 | $this->assertTrue($actualValue); 188 | } 189 | 190 | #[Test] 191 | public function it_returns_true_during_active_period_dismissed_in_past() 192 | { 193 | $now = CarbonImmutable::now(); 194 | 195 | /** @var Dismissible $dismissible */ 196 | $dismissible = Dismissible::factory() 197 | ->active(CarbonPeriod::create($now->subWeek(), $now->addWeeks(2))) 198 | ->create(); 199 | 200 | Dismissal::factory() 201 | ->for($dismissible) 202 | ->for($this->dismisser, 'dismisser') 203 | ->create([ 204 | 'dismissed_until' => $now->subDay(), 205 | ]); 206 | 207 | $actualValue = Dismissibles::shouldBeVisible($dismissible->name, $this->dismisser); 208 | 209 | $this->assertTrue($actualValue); 210 | } 211 | 212 | #[Test] 213 | public function it_returns_false_during_active_period_dismissed_in_future() 214 | { 215 | $now = CarbonImmutable::now(); 216 | 217 | /** @var Dismissible $dismissible */ 218 | $dismissible = Dismissible::factory() 219 | ->active(CarbonPeriod::create($now->subWeek(), $now->addWeeks(2))) 220 | ->create(); 221 | 222 | Dismissal::factory() 223 | ->for($dismissible) 224 | ->for($this->dismisser, 'dismisser') 225 | ->create([ 226 | 'dismissed_until' => $now->addDay(), 227 | ]); 228 | 229 | $actualValue = Dismissibles::shouldBeVisible($dismissible->name, $this->dismisser); 230 | 231 | $this->assertFalse($actualValue); 232 | } 233 | 234 | #[Test] 235 | public function it_returns_false_during_active_period_dismissed_forever() 236 | { 237 | $now = CarbonImmutable::now(); 238 | 239 | /** @var Dismissible $dismissible */ 240 | $dismissible = Dismissible::factory() 241 | ->active(CarbonPeriod::create($now->subWeek(), $now->addWeeks(2))) 242 | ->create(); 243 | 244 | Dismissal::factory() 245 | ->for($dismissible) 246 | ->for($this->dismisser, 'dismisser') 247 | ->create([ 248 | 'dismissed_until' => null, 249 | ]); 250 | 251 | $actualValue = Dismissibles::shouldBeVisible($dismissible->name, $this->dismisser); 252 | 253 | $this->assertFalse($actualValue); 254 | } 255 | 256 | #[Test] 257 | public function it_returns_false_after_active_period_not_dismissed() 258 | { 259 | $now = CarbonImmutable::now(); 260 | 261 | /** @var Dismissible $dismissible */ 262 | $dismissible = Dismissible::factory() 263 | ->active(CarbonPeriod::create($now->subDay(), $now->subSecond())) 264 | ->create(); 265 | 266 | $actualValue = Dismissibles::shouldBeVisible($dismissible->name, $this->dismisser); 267 | 268 | $this->assertFalse($actualValue); 269 | } 270 | 271 | #[Test] 272 | public function it_returns_false_after_active_period_dismissed_in_past() 273 | { 274 | $now = CarbonImmutable::now(); 275 | 276 | /** @var Dismissible $dismissible */ 277 | $dismissible = Dismissible::factory() 278 | ->active(CarbonPeriod::create($now->subWeeks(2), $now->subWeek())) 279 | ->create(); 280 | 281 | Dismissal::factory() 282 | ->for($dismissible) 283 | ->for($this->dismisser, 'dismisser') 284 | ->create([ 285 | 'dismissed_until' => $now->subDay(), 286 | ]); 287 | 288 | $actualValue = Dismissibles::shouldBeVisible($dismissible->name, $this->dismisser); 289 | 290 | $this->assertFalse($actualValue); 291 | } 292 | 293 | #[Test] 294 | public function it_returns_false_after_active_period_dismissed_in_future() 295 | { 296 | $now = CarbonImmutable::now(); 297 | 298 | /** @var Dismissible $dismissible */ 299 | $dismissible = Dismissible::factory() 300 | ->active(CarbonPeriod::create($now->subWeeks(2), $now->subWeek())) 301 | ->create(); 302 | 303 | Dismissal::factory() 304 | ->for($dismissible) 305 | ->for($this->dismisser, 'dismisser') 306 | ->create([ 307 | 'dismissed_until' => $now->addDay(), 308 | ]); 309 | 310 | $actualValue = Dismissibles::shouldBeVisible($dismissible->name, $this->dismisser); 311 | 312 | $this->assertFalse($actualValue); 313 | } 314 | 315 | #[Test] 316 | public function it_returns_false_after_active_period_dismissed_forever() 317 | { 318 | $now = CarbonImmutable::now(); 319 | 320 | /** @var Dismissible $dismissible */ 321 | $dismissible = Dismissible::factory() 322 | ->active(CarbonPeriod::create($now->subWeeks(2), $now->subWeek())) 323 | ->create(); 324 | 325 | Dismissal::factory() 326 | ->for($dismissible) 327 | ->for($this->dismisser, 'dismisser') 328 | ->create([ 329 | 'dismissed_until' => null, 330 | ]); 331 | 332 | $actualValue = Dismissibles::shouldBeVisible($dismissible->name, $this->dismisser); 333 | 334 | $this->assertFalse($actualValue); 335 | } 336 | } 337 | -------------------------------------------------------------------------------- /tests/Unit/Models/Dismissal/DismissedUntilTest.php: -------------------------------------------------------------------------------- 1 | create([ 19 | 'dismissed_until' => Date::now(), 20 | ]); 21 | 22 | $this->assertTrue($dismissal->dismissed_until instanceof DateTimeInterface); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/Unit/Models/Dismissal/ExtraDataTest.php: -------------------------------------------------------------------------------- 1 | create([ 17 | 'extra_data' => null, 18 | ]); 19 | 20 | $this->assertNull($dismissal->extra_data); 21 | } 22 | 23 | #[Test] 24 | public function it_returns_array_when_database_value_is_not_null() 25 | { 26 | $dismissal = Dismissal::factory()->create([ 27 | 'extra_data' => ['something'], 28 | ]); 29 | 30 | $this->assertIsArray($dismissal->extra_data); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/Unit/Models/Dismissal/ScopeDismissedAtTest.php: -------------------------------------------------------------------------------- 1 | assertEmpty(Dismissal::dismissedAt()->get()); 18 | } 19 | 20 | #[Test] 21 | public function it_does_not_return_a_dismissal_when_dismissed_until_past_date_time() 22 | { 23 | Dismissal::factory()->create([ 24 | 'dismissed_until' => Carbon::yesterday(), 25 | ]); 26 | 27 | $this->assertEmpty(Dismissal::dismissedAt()->get()); 28 | } 29 | 30 | #[Test] 31 | public function it_does_not_return_a_dismissal_when_dismissed_until_equals_now() 32 | { 33 | $now = Carbon::now(); 34 | 35 | Carbon::setTestNow($now); 36 | 37 | Dismissal::factory()->create([ 38 | 'dismissed_until' => $now, 39 | ]); 40 | 41 | $this->assertEmpty(Dismissal::dismissedAt()->get()); 42 | } 43 | 44 | #[Test] 45 | public function it_returns_a_dismissal_when_dismissed_until_future_date_time() 46 | { 47 | Dismissal::factory()->create([ 48 | 'dismissed_until' => Carbon::tomorrow(), 49 | ]); 50 | 51 | $this->assertNotEmpty(Dismissal::dismissedAt()->get()); 52 | } 53 | 54 | #[Test] 55 | public function it_returns_a_dismissal_when_dismissed_until_forever() 56 | { 57 | Dismissal::factory()->create([ 58 | 'dismissed_until' => null, 59 | ]); 60 | 61 | $this->assertNotEmpty(Dismissal::dismissedAt()->get()); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/Unit/Models/Dismissal/ScopeDismissedByTest.php: -------------------------------------------------------------------------------- 1 | dismisser = TestDismisserTypeOne::factory()->create(); 23 | } 24 | 25 | #[Test] 26 | public function it_does_not_return_a_dismissal_when_not_dismissed() 27 | { 28 | $this->assertEmpty(Dismissal::dismissedBy($this->dismisser)->get()); 29 | } 30 | 31 | #[Test] 32 | public function it_does_not_return_a_dismissal_when_dismissed_by_same_type_other_id() 33 | { 34 | Dismissal::factory() 35 | ->for(TestDismisserTypeOne::factory()->create(), 'dismisser') 36 | ->create(); 37 | 38 | $this->assertEmpty(Dismissal::dismissedBy($this->dismisser)->get()); 39 | } 40 | 41 | #[Test] 42 | public function it_does_not_return_a_dismissal_when_dismissed_by_same_id_other_type() 43 | { 44 | TestDismisserTypeOne::truncate(); 45 | TestDismisserTypeTwo::truncate(); 46 | 47 | $dismisserTypeOne = TestDismisserTypeOne::factory()->create(); 48 | $dismisserTypeTwo = TestDismisserTypeTwo::factory()->create(); 49 | 50 | $this->assertEquals($dismisserTypeOne->id, $dismisserTypeTwo->id); 51 | 52 | Dismissal::factory() 53 | ->for($dismisserTypeTwo, 'dismisser') 54 | ->create(); 55 | 56 | $this->assertEmpty(Dismissal::dismissedBy($this->dismisser)->get()); 57 | } 58 | 59 | #[Test] 60 | public function it_returns_a_dismissal_when_dismissed_by_same_type_and_id() 61 | { 62 | Dismissal::factory() 63 | ->for($this->dismisser, 'dismisser') 64 | ->create(); 65 | 66 | $this->assertNotEmpty(Dismissal::dismissedBy($this->dismisser)->get()); 67 | } 68 | 69 | #[Test] 70 | public function it_returns_the_correct_dismissal_when_dismissed_by_same_type_and_id() 71 | { 72 | $dismissal = Dismissal::factory() 73 | ->for($this->dismisser, 'dismisser') 74 | ->create(); 75 | 76 | $actualDismissals = Dismissal::dismissedBy($this->dismisser)->get(); 77 | 78 | $this->assertCount(1, $actualDismissals); 79 | $this->assertTrue($dismissal->is($actualDismissals->first())); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/Unit/Models/Dismissible/ActiveFromTest.php: -------------------------------------------------------------------------------- 1 | create(); 18 | 19 | $this->assertTrue($dismissible->active_from instanceof CarbonImmutable); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/Unit/Models/Dismissible/ActiveUntilTest.php: -------------------------------------------------------------------------------- 1 | create([ 20 | 'active_until' => Carbon::now()->addDay(), 21 | ]); 22 | 23 | $this->assertTrue($dismissible->active_until instanceof CarbonImmutable); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/Unit/Models/Dismissible/IsDismissedByTest.php: -------------------------------------------------------------------------------- 1 | dismisser = TestDismisserTypeOne::factory()->create(); 25 | } 26 | 27 | #[Test] 28 | public function it_returns_false_when_dismisser_has_not_dismissed() 29 | { 30 | /** @var Dismissible $dismissible */ 31 | $dismissible = Dismissible::factory()->create(); 32 | 33 | $actualValue = $dismissible->isDismissedBy($this->dismisser); 34 | 35 | $this->assertFalse($actualValue); 36 | } 37 | 38 | #[Test] 39 | public function it_returns_false_when_dismisser_has_dismissed_different_dismissible_until_past_date() 40 | { 41 | Dismissal::factory() 42 | ->for($this->dismisser, 'dismisser') 43 | ->create([ 44 | 'dismissed_until' => Carbon::yesterday(), 45 | ]); 46 | 47 | /** @var Dismissible $dismissible */ 48 | $dismissible = Dismissible::factory()->create(); 49 | 50 | $actualValue = $dismissible->isDismissedBy($this->dismisser); 51 | 52 | $this->assertFalse($actualValue); 53 | } 54 | 55 | #[Test] 56 | public function it_returns_false_when_dismisser_has_dismissed_different_dismissible_until_future_date() 57 | { 58 | Dismissal::factory() 59 | ->for($this->dismisser, 'dismisser') 60 | ->create([ 61 | 'dismissed_until' => Carbon::tomorrow(), 62 | ]); 63 | 64 | /** @var Dismissible $dismissible */ 65 | $dismissible = Dismissible::factory()->create(); 66 | 67 | $actualValue = $dismissible->isDismissedBy($this->dismisser); 68 | 69 | $this->assertFalse($actualValue); 70 | } 71 | 72 | #[Test] 73 | public function it_returns_false_when_dismisser_has_dismissed_until_past_date() 74 | { 75 | /** @var Dismissible $dismissible */ 76 | $dismissible = Dismissible::factory()->create(); 77 | 78 | Dismissal::factory() 79 | ->for($dismissible) 80 | ->for($this->dismisser, 'dismisser') 81 | ->create([ 82 | 'dismissed_until' => Carbon::yesterday(), 83 | ]); 84 | 85 | $actualValue = $dismissible->isDismissedBy($this->dismisser); 86 | 87 | $this->assertFalse($actualValue); 88 | } 89 | 90 | #[Test] 91 | public function it_returns_false_when_dismisser_has_dismissed_until_future_date() 92 | { 93 | /** @var Dismissible $dismissible */ 94 | $dismissible = Dismissible::factory()->create(); 95 | 96 | Dismissal::factory() 97 | ->for($dismissible) 98 | ->for($this->dismisser, 'dismisser') 99 | ->create([ 100 | 'dismissed_until' => Carbon::tomorrow(), 101 | ]); 102 | 103 | $actualValue = $dismissible->isDismissedBy($this->dismisser); 104 | 105 | $this->assertTrue($actualValue); 106 | } 107 | 108 | #[Test] 109 | public function it_returns_the_correct_value_when_dismissal_with_same_id_but_different_type_exists() 110 | { 111 | TestDismisserTypeOne::truncate(); 112 | TestDismisserTypeTwo::truncate(); 113 | 114 | $dismisserOfTypeOne = TestDismisserTypeOne::factory()->create(); 115 | $dismisserOfTypeTwo = TestDismisserTypeTwo::factory()->create(); 116 | 117 | $this->assertEquals($dismisserOfTypeOne->id, $dismisserOfTypeTwo->id); 118 | 119 | /** @var Dismissible $dismissible */ 120 | $dismissible = Dismissible::factory()->active()->create(); 121 | 122 | Dismissal::factory() 123 | ->for($dismissible) 124 | ->for($dismisserOfTypeOne, 'dismisser') 125 | ->create([ 126 | 'dismissed_until' => Carbon::tomorrow(), 127 | ]); 128 | 129 | $this->assertTrue($dismissible->isDismissedBy($dismisserOfTypeOne)); 130 | $this->assertFalse($dismissible->isDismissedBy($dismisserOfTypeTwo)); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /tests/Unit/Models/Dismissible/ScopeActiveTest.php: -------------------------------------------------------------------------------- 1 | active(CarbonPeriod::create($now->addDay(), $now->addWeek())) 22 | ->create(); 23 | 24 | $actualValue = Dismissible::active()->get(); 25 | 26 | $this->assertEmpty($actualValue); 27 | } 28 | 29 | #[Test] 30 | public function it_returns_the_dismissible_on_active_period_start() 31 | { 32 | $now = CarbonImmutable::now(); 33 | 34 | Dismissible::factory() 35 | ->active(CarbonPeriod::create($now, $now->addMinute())) 36 | ->create(); 37 | 38 | $actualValue = Dismissible::active()->get(); 39 | 40 | $this->assertNotEmpty($actualValue); 41 | } 42 | 43 | #[Test] 44 | public function it_returns_the_dismissible_during_active_period_when_active_until_is_set() 45 | { 46 | $now = CarbonImmutable::now(); 47 | 48 | Dismissible::factory() 49 | ->active(CarbonPeriod::create($now->subMinute(), $now->addMinute())) 50 | ->create(); 51 | 52 | $actualValue = Dismissible::active()->get(); 53 | 54 | $this->assertNotEmpty($actualValue); 55 | } 56 | 57 | #[Test] 58 | public function it_returns_the_dismissible_during_active_period_when_active_until_is_null() 59 | { 60 | $now = CarbonImmutable::now(); 61 | 62 | Dismissible::factory() 63 | ->active(CarbonPeriod::create($now->subMinute())) 64 | ->create(); 65 | 66 | $actualValue = Dismissible::active()->get(); 67 | 68 | $this->assertNotEmpty($actualValue); 69 | } 70 | 71 | #[Test] 72 | public function it_does_not_return_the_dismissible_after_active_period() 73 | { 74 | $now = CarbonImmutable::now(); 75 | 76 | Dismissible::factory() 77 | ->active(CarbonPeriod::create($now->subWeek(), $now->subDay())) 78 | ->create(); 79 | 80 | $actualValue = Dismissible::active()->get(); 81 | 82 | $this->assertEmpty($actualValue); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /tests/Unit/Models/Dismissible/ScopeNotDismissedByTest.php: -------------------------------------------------------------------------------- 1 | dismisser = TestDismisserTypeOne::factory()->create(); 26 | } 27 | 28 | #[Test] 29 | public function it_returns_dismissibles_which_are_active_in_the_past(): void 30 | { 31 | Dismissible::factory()->active()->create(); 32 | 33 | $actualValue = Dismissible::notDismissedBy($this->dismisser)->get(); 34 | 35 | $this->assertNotEmpty($actualValue); 36 | } 37 | 38 | #[Test] 39 | public function it_returns_dismissibles_which_are_active_in_the_future(): void 40 | { 41 | $now = CarbonImmutable::now(); 42 | 43 | Dismissible::factory() 44 | ->active(CarbonPeriod::create($now->addDay(), $now->addWeek())) 45 | ->create(); 46 | 47 | $actualValue = Dismissible::notDismissedBy($this->dismisser)->get(); 48 | 49 | $this->assertNotEmpty($actualValue); 50 | } 51 | 52 | #[Test] 53 | public function it_returns_dismissibles_when_dismissed_in_the_past(): void 54 | { 55 | $dismissible = Dismissible::factory()->create(); 56 | 57 | Dismissal::factory() 58 | ->for($this->dismisser, 'dismisser') 59 | ->for($dismissible) 60 | ->create([ 61 | 'dismissed_until' => Carbon::yesterday(), 62 | ]); 63 | 64 | $actualValue = Dismissible::notDismissedBy($this->dismisser)->get(); 65 | 66 | $this->assertNotEmpty($actualValue); 67 | } 68 | 69 | #[Test] 70 | public function it_returns_dismissibles_when_dismissed_in_the_future_by_someone_else(): void 71 | { 72 | $dismissible = Dismissible::factory()->create(); 73 | 74 | Dismissal::factory() 75 | ->for($dismissible) 76 | ->create([ 77 | 'dismissed_until' => Carbon::yesterday(), 78 | ]); 79 | 80 | $actualValue = Dismissible::notDismissedBy($this->dismisser)->get(); 81 | 82 | $this->assertNotEmpty($actualValue); 83 | } 84 | 85 | #[Test] 86 | public function it_does_not_return_dismissibles_when_dismissed_until_future_date_time(): void 87 | { 88 | $dismissible = Dismissible::factory()->create(); 89 | 90 | Dismissal::factory() 91 | ->for($this->dismisser, 'dismisser') 92 | ->for($dismissible) 93 | ->create([ 94 | 'dismissed_until' => Carbon::tomorrow(), 95 | ]); 96 | 97 | $actualValue = Dismissible::notDismissedBy($this->dismisser)->get(); 98 | 99 | $this->assertEmpty($actualValue); 100 | } 101 | 102 | #[Test] 103 | public function it_does_not_return_dismissibles_when_dismissed_until_forever(): void 104 | { 105 | $dismissible = Dismissible::factory()->create(); 106 | 107 | Dismissal::factory() 108 | ->for($this->dismisser, 'dismisser') 109 | ->for($dismissible) 110 | ->create([ 111 | 'dismissed_until' => null, 112 | ]); 113 | 114 | $actualValue = Dismissible::notDismissedBy($this->dismisser)->get(); 115 | 116 | $this->assertEmpty($actualValue); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /tests/Unit/Traits/HasDismissibles/DismissalsTest.php: -------------------------------------------------------------------------------- 1 | create(); 19 | 20 | $dismissible = Dismissible::factory()->create(); 21 | 22 | Dismissal::factory(5) 23 | ->for($dismisser, 'dismisser') 24 | ->for($dismissible) 25 | ->create(); 26 | 27 | $actualValue = $dismisser->dismissals; 28 | 29 | $this->assertCount(5, $actualValue); 30 | 31 | /** @var Dismissal $dismissal */ 32 | foreach ($actualValue as $dismissal) { 33 | $this->assertTrue($dismissal->dismisser->is($dismisser)); 34 | $this->assertTrue($dismissal->dismissible->is($dismissible)); 35 | } 36 | } 37 | 38 | #[Test] 39 | public function it_returns_all_dismissals_by_a_dismisser_of_different_dismissibles() 40 | { 41 | /** @var TestDismisserTypeOne $dismisser */ 42 | $dismisser = TestDismisserTypeOne::factory()->create(); 43 | 44 | Dismissal::factory(5) 45 | ->for($dismisser, 'dismisser') 46 | ->create(); 47 | 48 | $actualValue = $dismisser->dismissals; 49 | 50 | $this->assertCount(5, $actualValue); 51 | 52 | /** @var Dismissal $dismissal */ 53 | foreach ($actualValue as $dismissal) { 54 | $this->assertTrue($dismissal->dismisser->is($dismisser)); 55 | } 56 | } 57 | 58 | #[Test] 59 | public function it_only_returns_dismissals_of_the_dismisser() 60 | { 61 | /** @var TestDismisserTypeOne $dismisser */ 62 | $dismisser = TestDismisserTypeOne::factory()->create(); 63 | 64 | /** @var Dismissible $dismissible */ 65 | $dismissible = Dismissible::factory()->create(); 66 | 67 | $expectedDismissal = Dismissal::factory() 68 | ->for($dismisser, 'dismisser') 69 | ->for($dismissible) 70 | ->create(); 71 | 72 | Dismissal::factory() 73 | ->for($dismissible) 74 | ->create(); 75 | 76 | Dismissal::factory()->create(); 77 | 78 | $actualValue = $dismisser->dismissals; 79 | 80 | $this->assertCount(1, $actualValue); 81 | 82 | /** @var Dismissal $actualDismissal */ 83 | $actualDismissal = $actualValue->first(); 84 | 85 | $this->assertTrue($actualDismissal->is($expectedDismissal)); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /tests/database/migrations/2024_04_21_153207_create_test_dismissers_tables.php: -------------------------------------------------------------------------------- 1 | tables as $table) { 15 | Schema::create($table, function (Blueprint $table) { 16 | $table->id(); 17 | $table->timestamps(); 18 | }); 19 | } 20 | } 21 | 22 | public function down(): void 23 | { 24 | foreach ($this->tables as $table) { 25 | Schema::dropIfExists($table); 26 | } 27 | } 28 | }; 29 | --------------------------------------------------------------------------------