├── .changelog ├── .github └── workflows │ ├── build-changelog.yaml │ ├── create-release.yaml │ ├── pr.yaml │ └── run-tests.yaml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── phpunit.xml ├── src └── Publishable.php └── tests ├── Post.php ├── PublishableTest.php ├── TestCase.php └── migrations └── 2017_07_29_163922_create_posts_table.php /.changelog: -------------------------------------------------------------------------------- 1 | '', // Remove v from the front of tags and releases. 5 | ]; 6 | -------------------------------------------------------------------------------- /.github/workflows/build-changelog.yaml: -------------------------------------------------------------------------------- 1 | name: Build Changelog # Workflow name displayed on GitHub 2 | 3 | on: 4 | workflow_dispatch: 5 | branches: 6 | - '*.x' 7 | 8 | jobs: 9 | new-release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | 13 | - name: Clone sources 14 | uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 0 # otherwise, you will failed to push refs to dest repo 17 | token: ${{ secrets.MY_PAT }} 18 | 19 | - name: Setup PHP 20 | uses: shivammathur/setup-php@v2 21 | with: 22 | php-version: 8.2 23 | extensions: curl, mbstring, zip, pcntl, iconv 24 | coverage: none 25 | 26 | - name: Install dependencies 27 | run: composer update --prefer-dist --no-interaction 28 | 29 | - name: Build Changelog 30 | run: | 31 | git config --local user.email "jaymeh@users.noreply.github.com" 32 | git config --local user.name "Jamie Sykes" 33 | composer run-script release 34 | 35 | - name: Push changes 36 | uses: ad-m/github-push-action@master 37 | with: 38 | github_token: ${{ secrets.MY_PAT }} 39 | branch: ${{ github.ref }} 40 | tags: true 41 | -------------------------------------------------------------------------------- /.github/workflows/create-release.yaml: -------------------------------------------------------------------------------- 1 | name: Build Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*.*.*" 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | with: 15 | token: ${{ secrets.MY_PAT }} 16 | 17 | - name: Setup PHP 18 | uses: shivammathur/setup-php@v2 19 | with: 20 | php-version: 8.2 21 | extensions: curl, mbstring, zip, pcntl, iconv 22 | coverage: none 23 | 24 | - name: Install dependencies 25 | run: composer update --prefer-dist --no-interaction 26 | 27 | - name: Download Changelog Script 28 | run: | 29 | wget https://github.com/jaymeh/conventional-changelog-parser/releases/download/v1.2.4/changelog-checker.phar 30 | chmod +x changelog-checker.phar 31 | 32 | - name: Get Release Notes 33 | run: ./changelog-checker.phar > ${{ github.workspace }}-CHANGELOG.txt 34 | 35 | - name: Release 36 | uses: softprops/action-gh-release@v1 37 | with: 38 | draft: false 39 | name: ${{github.ref_name}} 40 | body_path: ${{ github.workspace }}-CHANGELOG.txt 41 | generate_release_notes: false 42 | token: ${{ secrets.MY_PAT }} 43 | -------------------------------------------------------------------------------- /.github/workflows/pr.yaml: -------------------------------------------------------------------------------- 1 | name: Check Conventional PR 2 | 3 | on: 4 | pull_request: 5 | branches: [ 1.x, 2.x, 3.x, 4.x, 5.x ] 6 | 7 | jobs: 8 | build: 9 | name: Ensure Conventional Commits 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v2-beta 14 | - uses: beemojs/conventional-pr-action@v2 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yaml: -------------------------------------------------------------------------------- 1 | name: run-tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | tests: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | os: [ubuntu-latest] 12 | php: [8.2, 8.3, 8.4] 13 | laravel: [12.*] 14 | dependency-version: [prefer-lowest, prefer-stable] 15 | include: 16 | - laravel: 12.* 17 | testbench: 10.* 18 | 19 | name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} 20 | 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v3 24 | 25 | - name: Install SQLite 3 26 | run: | 27 | sudo apt-get update 28 | sudo apt-get install sqlite3 29 | 30 | - name: Setup PHP 31 | uses: shivammathur/setup-php@v2 32 | with: 33 | php-version: ${{ matrix.php }} 34 | extensions: curl, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, iconv 35 | coverage: none 36 | 37 | - name: Install dependencies 38 | run: | 39 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update 40 | composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction 41 | 42 | - name: Execute tests 43 | run: vendor/bin/phpunit -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.lock 3 | .phpunit.result.cache 4 | .phpunit.cache 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # Changelog 3 | 4 | All notable changes to this project will be documented in this file. 5 | 6 | 7 | ## [5.0.0](https://github.com/jaymeh/laravel-publishable/compare/4.0.0...5.0.0) (2025-03-31) 8 | 9 | ### ⚠ BREAKING CHANGES 10 | 11 | * Add support for Laravel 12. ([bc2618](https://github.com/jaymeh/laravel-publishable/commit/bc2618d8857e120dae1f8923af3a882882858ec5)) 12 | 13 | 14 | --- 15 | 16 | ## [4.0.0](https://github.com/jaymeh/laravel-publishable/compare/3.0.0...4.0.0) (2024-03-13) 17 | 18 | ### ⚠ BREAKING CHANGES 19 | 20 | * Bumps dependencies for Laravel 11 support ([8fc3fa](https://github.com/jaymeh/laravel-publishable/commit/8fc3fa81e63319ed76279e82d38483c7fd64d80e)) 21 | 22 | ### Features 23 | 24 | * Migrates phpunit configuration ([c6ffd7](https://github.com/jaymeh/laravel-publishable/commit/c6ffd7a7d21fdfd1dcf3890e3ed513e2c4514093)) 25 | 26 | ### Bug Fixes 27 | 28 | * Adds a new composer script for tests ([85d7f4](https://github.com/jaymeh/laravel-publishable/commit/85d7f4c54e3bc381f79bbe4337cfd6c90136c54e)) 29 | * Ignore phpunit cache ([5014db](https://github.com/jaymeh/laravel-publishable/commit/5014dbb183d542eff010b01448da4479ccc9eefe)) 30 | * Replace doc comments with attributes ready for PHPUnit 12 ([b0eff7](https://github.com/jaymeh/laravel-publishable/commit/b0eff76aab6c81f2d900249e2f3318c1cb9d8991)) 31 | 32 | 33 | --- 34 | 35 | ## [3.0.0](https://github.com/jaymeh/laravel-publishable/compare/2.1.0...3.0.0) (2023-03-20) 36 | 37 | ### ⚠ BREAKING CHANGES 38 | 39 | * Update dependencies to introduce Laravel 10 support. ([c326b9](https://github.com/jaymeh/laravel-publishable/commit/c326b90d5193c8a9d4bbaa6d0fefdf05aef84288)) 40 | 41 | 42 | --- 43 | 44 | ## [2.1.0](https://github.com/jaymeh/laravel-publishable/compare/2.0.0...2.1.0) (2022-09-27) 45 | 46 | ### Features 47 | 48 | * Allows publishing an unpublishing to not fire model events using new functions. ([4cd635](https://github.com/jaymeh/laravel-publishable/commit/4cd635cbb217bcdaf94fbf7990e042f42d6b20e8)) 49 | 50 | 51 | --- 52 | 53 | ## [2.0.0](https://github.com/jaymeh/laravel-publishable/compare/1.2.3...2.0.0) (2022-09-08) 54 | 55 | ### ⚠ BREAKING CHANGES 56 | 57 | * Force breaking change commit for Laravel 9 support. ([3fb000](https://github.com/jaymeh/laravel-publishable/commit/3fb0009b90df63b4249b047f0676d90f711d86ca)) 58 | 59 | ### Features 60 | 61 | * Install changelog tools. ([2d88d9](https://github.com/jaymeh/laravel-publishable/commit/2d88d91d946ab1631e3209d4fc4871df7fe07914)) 62 | * Make published_at field fillable by default. ([40c982](https://github.com/jaymeh/laravel-publishable/commit/40c98234c2f612cfe8402d54218a8736277e9d61)) 63 | * Remove the fillable functionality of the package. ([c0e77d](https://github.com/jaymeh/laravel-publishable/commit/c0e77d006c978f66c366726576b41b79bcbdfbf4)) 64 | 65 | ### Bug Fixes 66 | 67 | * Add conventional changelog library as dev dependency. ([e9fd01](https://github.com/jaymeh/laravel-publishable/commit/e9fd018dcbf4577326daba14a02b949114d259ef)) 68 | * Fixes an issue with incorrectly named CI job. ([0400dc](https://github.com/jaymeh/laravel-publishable/commit/0400dc0b4b6df55fdd0236cd9127aeb4255ec4cb)) 69 | * Remove orchestra/database dependency from package providers. ([ef5215](https://github.com/jaymeh/laravel-publishable/commit/ef5215cac0ec68b9b35061bc774bab4bd4345c3c)) 70 | * Remove orchestral/database from dependencies. ([262da5](https://github.com/jaymeh/laravel-publishable/commit/262da5d56915b124dfebd8a562978047ae53e89b)) 71 | * Upgrade PHPUnit Schema to latest version. ([5faa38](https://github.com/jaymeh/laravel-publishable/commit/5faa381c3a65f07dad4eba7e011eca9dbd873fb4)) 72 | 73 | 74 | --- 75 | 76 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Pawel Mysior 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Publishable 2 | 3 | Toggle the published state of your Eloquent models easily. 4 | 5 | ## Installation 6 | 7 | You can install the package via composer: 8 | 9 | ```bash 10 | composer require pawelmysior/laravel-publishable 11 | ``` 12 | 13 | ## Versions 14 | 15 | For details about which version of this package to use with your Laravel version please see the table below: 16 | 17 | | Laravel Version | Package Version | 18 | | --------------- | --------------- | 19 | | <9.x | 1.x | 20 | | 9.x | 2.x | 21 | | 10.x | 3.x | 22 | | 11.x | 4.x | 23 | | 12.x | 5.x | 24 | 25 | ## Preparation 26 | 27 | To start you need to add a `published_at` nullable timestamp column to your table. 28 | 29 | Put this in your table migration: 30 | 31 | ```php 32 | $table->timestamp('published_at')->nullable(); 33 | ``` 34 | 35 | Now use the trait on the model 36 | 37 | ```php 38 | get(); 62 | 63 | // Get only unpublished posts 64 | Post::unpublished()->get(); 65 | 66 | // Check if the post is published 67 | $post->isPublished(); 68 | 69 | // Check if the post is unpublished 70 | $post->isUnpublished(); 71 | 72 | // Publish the post 73 | $post->publish(); 74 | 75 | // Unpublish the post 76 | $post->unpublish(); 77 | 78 | // Publish the post without firing model events 79 | $post->publishQuietly(); 80 | 81 | // Unpublish the post without firing model events 82 | $post->unpublishQuietly(); 83 | ``` 84 | 85 | A post is considered published when the `published_at` is not null and in the past. 86 | 87 | A post is considered unpublished when the `published_at` is null or in the future. 88 | 89 | ## Security 90 | 91 | If you discover any security-related issues, please email security@jaymeh.co.uk instead of using the issue tracker. 92 | 93 | ## Contributing 94 | 95 | Any contributions to this repository are welcomed. Please be aware that we are using [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) to assist in self documentation and reduce manual work involved with releases. 96 | 97 | ## License 98 | 99 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 100 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pawelmysior/laravel-publishable", 3 | "description": "Toggle the published state of your Eloquent models easily", 4 | "keywords": [ 5 | "laravel", 6 | "eloquent", 7 | "trait" 8 | ], 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "Pawel Mysior", 13 | "email": "pawelmysior@gmail.com" 14 | }, 15 | { 16 | "name": "Jamie Sykes", 17 | "email": "jaymeh@jaymeh.co.uk" 18 | } 19 | ], 20 | "require": { 21 | "php": "^8.2", 22 | "illuminate/database": "^12.0", 23 | "nesbot/carbon": "^3.0" 24 | }, 25 | "require-dev": { 26 | "orchestra/testbench": "^10.0", 27 | "phpunit/phpunit": "^11.5.3", 28 | "marcocesarato/php-conventional-changelog": "^1.17" 29 | }, 30 | "autoload": { 31 | "psr-4": { 32 | "PawelMysior\\Publishable\\": "src/" 33 | } 34 | }, 35 | "autoload-dev": { 36 | "psr-4": { 37 | "PawelMysior\\Publishable\\Tests\\": "tests/" 38 | } 39 | }, 40 | "scripts": { 41 | "test": "phpunit", 42 | "changelog": "conventional-changelog", 43 | "release": "conventional-changelog --commit", 44 | "release:patch": "conventional-changelog --patch --commit", 45 | "release:minor": "conventional-changelog --minor --commit", 46 | "release:major": "conventional-changelog --major --commit" 47 | }, 48 | "version": "5.0.0" 49 | } -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./tests 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | ./src 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/Publishable.php: -------------------------------------------------------------------------------- 1 | where('published_at', '<=', Carbon::now())->whereNotNull('published_at'); 19 | } 20 | 21 | /** 22 | * Scope a query to only include unpublished models. 23 | * 24 | * @param \Illuminate\Database\Eloquent\Builder $query 25 | * @return \Illuminate\Database\Eloquent\Builder 26 | */ 27 | public function scopeUnpublished(Builder $query) 28 | { 29 | return $query->where('published_at', '>', Carbon::now())->orWhereNull('published_at'); 30 | } 31 | 32 | /** 33 | * @return bool 34 | */ 35 | public function isPublished() 36 | { 37 | if (is_null($this->published_at)) { 38 | return false; 39 | } 40 | 41 | return $this->published_at->lte(Carbon::now()); 42 | } 43 | 44 | /** 45 | * @return bool 46 | */ 47 | public function isUnpublished() 48 | { 49 | return !$this->isPublished(); 50 | } 51 | 52 | /** 53 | * @return bool 54 | */ 55 | public function publish() 56 | { 57 | $this->published_at = Carbon::now(); 58 | 59 | return $this->save(); 60 | } 61 | 62 | /** 63 | * Publishes the model without firing events. 64 | * 65 | * @return bool 66 | */ 67 | public function publishQuietly() 68 | { 69 | $this->published_at = Carbon::now(); 70 | 71 | return $this->saveQuietly(); 72 | } 73 | 74 | /** 75 | * @return bool 76 | */ 77 | public function unpublish() 78 | { 79 | $this->published_at = null; 80 | 81 | return $this->save(); 82 | } 83 | 84 | /** 85 | * Unpublishes the model without firing events. 86 | * 87 | * @return bool 88 | */ 89 | public function unpublishQuietly() 90 | { 91 | $this->published_at = null; 92 | 93 | return $this->saveQuietly(); 94 | } 95 | 96 | /** 97 | * @param mixed $value 98 | * @return \Carbon\Carbon 99 | */ 100 | public function getPublishedAtAttribute($value) 101 | { 102 | if (is_null($value)) { 103 | return $value; 104 | } 105 | 106 | return $this->asDateTime($value); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /tests/Post.php: -------------------------------------------------------------------------------- 1 | Carbon::yesterday(), 16 | ]); 17 | 18 | $this->assertTrue($post->isPublished()); 19 | $this->assertFalse($post->isUnpublished()); 20 | } 21 | 22 | #[Test] 23 | public function posts_with_published_at_date_set_as_now_are_published() 24 | { 25 | $post = Post::create([ 26 | 'published_at' => Carbon::now(), 27 | ]); 28 | 29 | $this->assertTrue($post->isPublished()); 30 | $this->assertFalse($post->isUnpublished()); 31 | } 32 | 33 | #[Test] 34 | public function posts_with_published_at_date_in_the_future_are_not_published() 35 | { 36 | $post = Post::create([ 37 | 'published_at' => Carbon::tomorrow(), 38 | ]); 39 | 40 | $this->assertFalse($post->isPublished()); 41 | $this->assertTrue($post->isUnpublished()); 42 | } 43 | 44 | #[Test] 45 | public function posts_with_published_at_set_as_null_are_not_published() 46 | { 47 | $post = Post::create([ 48 | 'published_at' => null, 49 | ]); 50 | 51 | $this->assertFalse($post->isPublished()); 52 | $this->assertTrue($post->isUnpublished()); 53 | } 54 | 55 | #[Test] 56 | public function publishing_post_sets_the_published_at_date_as_now() 57 | { 58 | $post = Post::create([ 59 | 'published_at' => null, 60 | ]); 61 | 62 | $post->publish(); 63 | 64 | $this->assertEquals(Carbon::now()->timestamp, $post->published_at->timestamp); 65 | } 66 | 67 | #[Test] 68 | public function unpublishing_post_sets_the_published_at_date_as_null() 69 | { 70 | $post = Post::create([ 71 | 'published_at' => Carbon::now(), 72 | ]); 73 | 74 | $post->unpublish(); 75 | 76 | $this->assertNull($post->published_at); 77 | } 78 | 79 | #[Test] 80 | public function test_published_scope() 81 | { 82 | $firstPublishedPost = Post::create([ 83 | 'published_at' => Carbon::yesterday(), 84 | ]); 85 | $secondPublishedPost = Post::create([ 86 | 'published_at' => Carbon::now(), 87 | ]); 88 | 89 | $firstUnpublishedPost = Post::create([ 90 | 'published_at' => null, 91 | ]); 92 | $secondUnpublishedPost = Post::create([ 93 | 'published_at' => Carbon::tomorrow(), 94 | ]); 95 | 96 | $posts = Post::published()->get(); 97 | 98 | $this->assertTrue($posts->contains($firstPublishedPost)); 99 | $this->assertTrue($posts->contains($secondPublishedPost)); 100 | $this->assertFalse($posts->contains($firstUnpublishedPost)); 101 | $this->assertFalse($posts->contains($secondUnpublishedPost)); 102 | } 103 | 104 | #[Test] 105 | public function test_unpublished_scope() 106 | { 107 | $firstPublishedPost = Post::create([ 108 | 'published_at' => Carbon::yesterday(), 109 | ]); 110 | $secondPublishedPost = Post::create([ 111 | 'published_at' => Carbon::now(), 112 | ]); 113 | 114 | $firstUnpublishedPost = Post::create([ 115 | 'published_at' => null, 116 | ]); 117 | $secondUnpublishedPost = Post::create([ 118 | 'published_at' => Carbon::tomorrow(), 119 | ]); 120 | 121 | $posts = Post::unpublished()->get(); 122 | 123 | $this->assertFalse($posts->contains($firstPublishedPost)); 124 | $this->assertFalse($posts->contains($secondPublishedPost)); 125 | $this->assertTrue($posts->contains($firstUnpublishedPost)); 126 | $this->assertTrue($posts->contains($secondUnpublishedPost)); 127 | } 128 | 129 | #[Test] 130 | public function test_published_at_field_is_not_fillable() 131 | { 132 | $post = new Post(); 133 | $this->assertNotContains('published_at', $post->getFillable()); 134 | } 135 | 136 | #[Test] 137 | public function test_publish_quietly_function_does_not_fire_model_events() 138 | { 139 | // Fake events so we can test on them. 140 | Event::fake(); 141 | 142 | // Create a post. 143 | $post = Post::create([ 144 | 'published_at' => Carbon::yesterday(), 145 | ]); 146 | 147 | // Run publish Quietly Function. 148 | $post->publishQuietly(); 149 | 150 | // Assert it doesn't trigger the updated event. 151 | Event::assertNotDispatched('eloquent.updated: PawelMysior\Publishable\Tests\Post'); 152 | } 153 | 154 | #[Test] 155 | public function test_unpublish_quietly_function_does_not_fire_model_events() 156 | { 157 | // Fake events so we can test on them. 158 | Event::fake(); 159 | 160 | // Create a post. 161 | $post = Post::create([ 162 | 'published_at' => Carbon::yesterday(), 163 | ]); 164 | 165 | // Run publish Quietly Function. 166 | $post->unpublishQuietly(); 167 | 168 | // Assert it doesn't trigger the updated event. 169 | Event::assertNotDispatched('eloquent.updated: PawelMysior\Publishable\Tests\Post'); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | loadMigrationsFrom(realpath(__DIR__.'/migrations')); 14 | } 15 | 16 | protected function getEnvironmentSetUp($app) 17 | { 18 | $app['config']->set('database.default', 'testing'); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/migrations/2017_07_29_163922_create_posts_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->timestamp('published_at')->nullable(); 19 | $table->timestamps(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | Schema::dropIfExists('posts'); 31 | } 32 | } 33 | --------------------------------------------------------------------------------