├── .php-cs-fixer.dist.php ├── .pre-commit-config.yaml ├── .pre-commit ├── phpcs.sh └── rector.sh ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── SECURITY.md ├── composer.json ├── config └── drafts.php ├── database └── factories │ ├── ModelFactory.php │ ├── PostFactory.php │ ├── PostSectionFactory.php │ ├── SoftDeletingPostFactory.php │ └── TagFactory.php ├── rector.php ├── resources └── views │ └── .gitkeep └── src ├── Concerns ├── HasDrafts.php └── Publishes.php ├── Facades └── LaravelDrafts.php ├── Http └── Middleware │ └── WithDraftsMiddleware.php ├── LaravelDrafts.php ├── LaravelDraftsServiceProvider.php └── Scopes └── PublishingScope.php /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | in([ 5 | __DIR__ . '/src', 6 | __DIR__ . '/tests', 7 | ]) 8 | ->name('*.php') 9 | ->notName('*.blade.php') 10 | ->ignoreDotFiles(true) 11 | ->ignoreVCS(true); 12 | 13 | return (new PhpCsFixer\Config()) 14 | ->setRules([ 15 | '@PSR12' => true, 16 | 'array_syntax' => ['syntax' => 'short'], 17 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 18 | 'no_unused_imports' => true, 19 | 'not_operator_with_successor_space' => true, 20 | 'trailing_comma_in_multiline' => true, 21 | 'phpdoc_scalar' => true, 22 | 'unary_operator_spaces' => true, 23 | 'binary_operator_spaces' => true, 24 | 'blank_line_before_statement' => [ 25 | 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], 26 | ], 27 | 'phpdoc_single_line_var_spacing' => true, 28 | 'phpdoc_var_without_name' => true, 29 | 'class_attributes_separation' => [ 30 | 'elements' => [ 31 | 'method' => 'one', 32 | ], 33 | ], 34 | 'method_argument_space' => [ 35 | 'on_multiline' => 'ensure_fully_multiline', 36 | 'keep_multiple_spaces_after_comma' => true, 37 | ], 38 | 'single_trait_insert_per_statement' => true, 39 | ]) 40 | ->setFinder($finder); 41 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: ^(legacy/|app/functions/) 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v4.3.0 5 | hooks: 6 | - id: check-byte-order-marker # Forbid UTF-8 byte-order markers 7 | # Check for files with names that would conflict on a case-insensitive 8 | # filesystem like MacOS HFS+ or Windows FAT. 9 | - id: check-case-conflict 10 | - id: check-json 11 | - id: check-yaml 12 | - id: end-of-file-fixer 13 | - id: trailing-whitespace 14 | - id: mixed-line-ending 15 | - repo: https://github.com/digitalpulp/pre-commit-php 16 | rev: 1.4.0 17 | hooks: 18 | - id: php-lint 19 | - repo: local 20 | hooks: 21 | - id: donotcommit 22 | name: DO NOT COMMIT check 23 | entry: 'do not commit|DO NOT COMMIT' 24 | args: [ --multiline ] 25 | language: pygrep 26 | types: [ text ] 27 | - id: checkfordebugging 28 | name: check for debugging methods 29 | description: 'Check the dd(), debug(), dump() or any ray() methods have not been left in' 30 | entry: '[\s>@]dd\(|[\s>@]debug\(|[\s>@]dump\(|[\s>@]ray\(|[\s>@]rd\(|[\s]die\(|[\s]exit\(' 31 | language: pygrep 32 | types: [ text ] 33 | - id: pint 34 | name: Laravel Pint 35 | description: Run rector against all staged PHP files. 36 | files: \.php$ 37 | entry: .pre-commit/rector.sh 38 | language: script 39 | - id: rector 40 | name: Format PHP files 41 | description: 'Format PHP files with php-cs-fixer' 42 | files: \.php$ 43 | entry: .pre-commit/phpcs.sh 44 | language: script 45 | -------------------------------------------------------------------------------- /.pre-commit/phpcs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ################################################################################ 3 | 4 | RED='\033[0;31m' 5 | BACKGROUND_RED='\033[0;41m' 6 | YELLOW='\033[0;33m' 7 | BOLD_YELLOW='\033[1;33m' 8 | NC='\033[0m' # No Color 9 | 10 | command_files_to_check="${@:2}" 11 | command_args=$1 12 | command_to_run="./vendor/bin/php-cs-fixer fix --allow-risky=yes ${command_args} ${command_files_to_check}" 13 | 14 | command_result=`eval $command_to_run` 15 | 16 | if [[ "$command_result" == *FAIL* ]]; then 17 | echo "$command_result" 18 | echo -e "${BACKGROUND_RED} PHPCS failed ${RED} PHPCS was unable to fix some issues in your files. \ 19 | Please fix the errors and try again.${NC}" 20 | exit 3 21 | fi 22 | 23 | if [[ "$command_result" == *FIXED* ]]; then 24 | echo -e "${BOLD_YELLOW} PHPCS fixed some issues in your files.${YELLOW} Please re-stage them and try again.${NC}" 25 | exit 1 26 | fi 27 | 28 | exit 0 29 | -------------------------------------------------------------------------------- /.pre-commit/rector.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ################################################################################ 3 | 4 | RED='\033[0;31m' 5 | BACKGROUND_RED='\033[0;41m' 6 | YELLOW='\033[0;33m' 7 | BOLD_YELLOW='\033[1;33m' 8 | NC='\033[0m' # No Color 9 | 10 | command_files_to_check="${@:2}" 11 | command_args=$1 12 | command_to_run="./vendor/bin/rector ${command_args} ${command_files_to_check}" 13 | 14 | command_result=`eval $command_to_run` 15 | 16 | if [[ "$command_result" == *FAIL* ]]; then 17 | echo "$command_result" 18 | echo -e "${BACKGROUND_RED} Pint failed ${RED} Pint was unable to fix some issues in your files. \ 19 | Please fix the errors and try again.${NC}" 20 | exit 3 21 | fi 22 | 23 | if [[ "$command_result" == *FIXED* ]]; then 24 | echo -e "${BOLD_YELLOW} Pint fixed some issues in your files.${YELLOW} Please re-stage them and try again.${NC}" 25 | exit 1 26 | fi 27 | 28 | exit 0 29 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-drafts` will be documented in this file. 4 | 5 | ## v1.2.0 - 2023-02-22 6 | 7 | ### What's Changed 8 | 9 | - New feature: preview mode by @oddvalue in https://github.com/oddvalue/laravel-drafts/pull/19 10 | - Add support for Laravel 10 by @oddvalue in https://github.com/oddvalue/laravel-drafts/pull/20 11 | 12 | **Full Changelog**: https://github.com/oddvalue/laravel-drafts/compare/v1.1.0...v1.2.0 13 | 14 | ## v1.1.0 - 2023-02-20 15 | 16 | ### What's Changed 17 | 18 | - Added a method to avoid creating revison by @Froxz in https://github.com/oddvalue/laravel-drafts/pull/15 19 | 20 | ### New Contributors 21 | 22 | - @Froxz made their first contribution in https://github.com/oddvalue/laravel-drafts/pull/15 23 | 24 | **Full Changelog**: https://github.com/oddvalue/laravel-drafts/compare/v1.0.2...v1.1.0 25 | 26 | ## v1.0.2 - 2023-02-17 27 | 28 | **Full Changelog**: https://github.com/oddvalue/laravel-drafts/compare/v1.0.1...v1.0.2 29 | 30 | ## v0.0.3 - 2022-07-01 31 | 32 | **Full Changelog**: https://github.com/oddvalue/laravel-drafts/compare/v0.0.2...v0.0.3 33 | 34 | - Add support for drafting with relations 35 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) oddvalue 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 | ![](https://banners.beyondco.de/Laravel%20Drafts.png?theme=dark&packageManager=composer+require&packageName=oddvalue%2Flaravel-drafts&pattern=architect&style=style_1&description=A+simple%2C+drop-in+drafts%2Frevisions+system+for+Laravel+models&md=1&showWatermark=1&fontSize=100px&images=https%3A%2F%2Flaravel.com%2Fimg%2Flogomark.min.svg "Laravel Drafts") 2 | 3 | 4 | 5 | # A simple, drop-in drafts/revisions system for Laravel models 6 | 7 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/oddvalue/laravel-drafts.svg?style=flat-square)](https://packagist.org/packages/oddvalue/laravel-drafts) 8 | ![PHP Support](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Foddvalue%2Flaravel-drafts%2Fmain%2Fcomposer.json&query=require.php&label=PHP) 9 | ![Laravel Support](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Foddvalue%2Flaravel-drafts%2Fmain%2Fcomposer.json&query=require%5B'illuminate%2Fcontracts'%5D&label=Laravel) 10 | [![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/oddvalue/laravel-drafts/run-tests.yml?label=tests&style=flat-square)](https://github.com/oddvalue/laravel-drafts/actions?query=workflow%3Arun-tests+branch%3Amain) 11 | [![GitHub Code Style Action Status](https://img.shields.io/github/actions/workflow/status/oddvalue/laravel-drafts/php-cs-fixer.yml?label=code%20style&style=flat-square)](https://github.com/oddvalue/laravel-drafts/actions?query=workflow%3A"Check+%26+fix+styling"+branch%3Amain) 12 | [![Total Downloads](https://img.shields.io/packagist/dt/oddvalue/laravel-drafts.svg?style=flat-square)](https://packagist.org/packages/oddvalue/laravel-drafts) 13 | ![Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/oddvalue/9dd8e508cb2433728d42a258193770eb/raw/laravel-drafts-cobertura-coverage.json) 14 | 15 | * [Installation](#installation) 16 | * [Usage](#usage) 17 | + [Preparing your models](#preparing-your-models) 18 | - [Add the trait](#add-the-trait) 19 | - [Relations](#relations) 20 | - [Database](#database) 21 | + [The API](#the-api) 22 | - [Creating a new record](#creating-a-new-record) 23 | - [Relations](#relations) 24 | + [Interacting with records](#interacting-with-records) 25 | - [Published revision](#published-revision) 26 | - [Current Revision](#current-revision) 27 | - [Revisions](#revisions) 28 | - [Preview mode](#preview-mode) 29 | + [Middleware](#middleware) 30 | - [WithDraftsMiddleware](#withdraftsmiddleware) 31 | * [Testing](#testing) 32 | * [Changelog](#changelog) 33 | * [Contributing](#contributing) 34 | * [Security Vulnerabilities](#security-vulnerabilities) 35 | * [Credits](#credits) 36 | * [License](#license) 37 | 38 | ## Version compatibility 39 | 40 | | Laravel | Drafts | 41 | |---------|--------| 42 | | v9.x | v1.x | 43 | | v10.x | v1.x | 44 | | v11.x | v2.x | 45 | | v12.x | >2.1 | 46 | 47 | ## Installation 48 | 49 | You can install the package via composer: 50 | 51 | ```bash 52 | composer require oddvalue/laravel-drafts 53 | ``` 54 | 55 | You can publish the config file with: 56 | 57 | ```bash 58 | php artisan vendor:publish --tag="drafts-config" 59 | ``` 60 | 61 | This is the contents of the published config file: 62 | 63 | ```php 64 | return [ 65 | 'revisions' => [ 66 | 'keep' => 10, 67 | ], 68 | 69 | 'column_names' => [ 70 | /* 71 | * Boolean column that marks a row as the current version of the data for editing. 72 | */ 73 | 'is_current' => 'is_current', 74 | 75 | /* 76 | * Boolean column that marks a row as live and displayable to the public. 77 | */ 78 | 'is_published' => 'is_published', 79 | 80 | /* 81 | * Timestamp column that stores the date and time when the row was published. 82 | */ 83 | 'published_at' => 'published_at', 84 | 85 | /* 86 | * UUID column that stores the unique identifier of the model drafts. 87 | */ 88 | 'uuid' => 'uuid', 89 | 90 | /* 91 | * Name of the morph relationship to the publishing user. 92 | */ 93 | 'publisher_morph_name' => 'publisher', 94 | ], 95 | 96 | 'auth' => [ 97 | /* 98 | * The guard to fetch the logged-in user from for the publisher relation. 99 | */ 100 | 'guard' => 'web', 101 | ], 102 | ]; 103 | ``` 104 | 105 | ## Usage 106 | 107 | ### Preparing your models 108 | 109 | #### Add the trait 110 | 111 | Add the `HasDrafts` trait to your model 112 | 113 | ```php 114 | drafts(); 186 | }); 187 | 188 | Schema::table('posts', function (Blueprint $table) { 189 | $table->dropDrafts(); 190 | }); 191 | ``` 192 | 193 | ### The API 194 | 195 | The `HasDrafts` trait will add a default scope that will only return published/live records. 196 | 197 | The following query builder methods are available to alter this behavior: 198 | 199 | * `withoutDrafts()`/`published(bool $withoutDrafts = true)` Only select published records (default) 200 | * `withDrafts(bool $withDrafts = false)` Include draft record 201 | * `onlyDrafts()` Select only drafts, exclude published 202 | 203 | #### Creating a new record 204 | 205 | By default, new records will be created as published. You can change this either by including `'is_published' => false` in the attributes of the model or by using the `createDraft` or `saveAsDraft` methods. 206 | 207 | ```php 208 | Post::create([ 209 | 'title' => 'Foo', 210 | 'is_published' => false, 211 | ]); 212 | 213 | # OR 214 | 215 | Post::createDraft(['title' => 'Foo']); 216 | 217 | # OR 218 | 219 | Post::make(['title' => 'Foo'])->saveAsDraft(); 220 | ``` 221 | 222 | When saving/updating a record the published state will be maintained. If you want to save a draft of a published record then you can use the `saveAsDraft` and `updateAsDraft` methods. 223 | 224 | ```php 225 | # Create published post 226 | $post = Post::create(['title' => 'Foo']); 227 | 228 | # Create drafted copy 229 | 230 | $post->updateAsDraft(['title' => 'Bar']); 231 | 232 | # OR 233 | 234 | $post->title = 'Bar'; 235 | $post->saveAsDraft(); 236 | ``` 237 | 238 | This will create a draft record and the original record will be left unchanged. 239 | 240 | | # | title | uuid | published_at | is_published | is_current | created_at | updated_at | 241 | |---|-------|--------------------------------------|---------------------|--------------|------------|---------------------|---------------------| 242 | | 1 | Foo | 9188eb5b-cc42-47e9-aec3-d396666b4e80 | 2000-01-01 00:00:00 | 1 | 0 | 2000-01-01 00:00:00 | 2000-01-01 00:00:00 | 243 | | 2 | Bar | 9188eb5b-cc42-47e9-aec3-d396666b4e80 | 2000-01-02 00:00:00 | 0 | 1 | 2000-01-02 00:00:00 | 2000-01-02 00:00:00 | 244 | 245 | ### Interacting with records 246 | 247 | #### Published revision 248 | 249 | The published revision if the live version of the record and will be the one that is displayed to the public. The default behavior is to only show the published revision. 250 | 251 | ```php 252 | # Get all published posts 253 | $posts = Post::all(); 254 | ``` 255 | 256 | #### Current Revision 257 | 258 | Every record will have a current revision. That is the most recent revision and what you would want to display in your 259 | admin. 260 | 261 | To fetch the current revision you can call the `current` scope. 262 | 263 | ```php 264 | $posts = Post::current()->get(); 265 | ``` 266 | 267 | #### Revisions 268 | 269 | Every time a record is updated a new row/revision will be inserted. The default number of revisions kept is 10, this can be updated in the published config file. 270 | 271 | You can fetch the revisions of a record by calling the `revisions` method. 272 | 273 | ```php 274 | $post = Post::find(1); 275 | $revisions = $post->revisions(); 276 | ``` 277 | 278 | Deleting a record will also delete all of its revisions. Soft deleting records will soft delete the revisions and restoring records will restore the revisions. 279 | 280 | If you need to update a record without creating revision 281 | 282 | ```php 283 | $post->withoutRevision()->update($options); 284 | ``` 285 | 286 | #### Preview Mode 287 | 288 | Enabling preview mode will disable the global scope that fetches only published records and will instead fetch the current revision regardless of published state. 289 | 290 | ```php 291 | # Enable preview mode 292 | \Oddvalue\LaravelDrafts\Facades\LaravelDrafts::previewMode(); 293 | \Oddvalue\LaravelDrafts\Facades\LaravelDrafts::previewMode(true); 294 | 295 | # Disable preview mode 296 | \Oddvalue\LaravelDrafts\Facades\LaravelDrafts::disablePreviewMode(); 297 | \Oddvalue\LaravelDrafts\Facades\LaravelDrafts::previewMode(false); 298 | ``` 299 | 300 | ### Middleware 301 | 302 | #### WithDraftsMiddleware 303 | 304 | If you require a specific route to be able to access drafts then you can use the `WithDraftsMiddleware` middleware. 305 | 306 | ```php 307 | Route::get('/posts/publish/{post}', [PostController::class, 'publish'])->middleware(\Oddvalue\LaravelDrafts\Http\Middleware\WithDraftsMiddleware::class); 308 | ``` 309 | 310 | There is also a helper method on the router that allows you to create a group with that middleware applied. 311 | 312 | ```php 313 | Route::withDrafts(function (): void { 314 | Route::get('/posts/publish/{post}', [PostController::class, 'publish']); 315 | }); 316 | ``` 317 | 318 | ## Testing 319 | 320 | ```bash 321 | composer test 322 | ``` 323 | 324 | ## Changelog 325 | 326 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 327 | 328 | ## Contributing 329 | 330 | Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. 331 | 332 | ## Security Vulnerabilities 333 | 334 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 335 | 336 | ## Credits 337 | 338 | - [jim](https://github.com/oddvalue) 339 | - [All Contributors](../../contributors) 340 | 341 | ## License 342 | 343 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 344 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | If you discover any security-related issues, please email security@oddvalue.co.uk instead of using the issue tracker. 6 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oddvalue/laravel-drafts", 3 | "description": "A simple, drop-in drafts/revisions system for Laravel models", 4 | "keywords": [ 5 | "oddvalue", 6 | "laravel", 7 | "drafts", 8 | "revisions" 9 | ], 10 | "homepage": "https://github.com/oddvalue/laravel-drafts", 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "jim", 15 | "email": "jim@oddvalue.co.uk", 16 | "role": "Developer" 17 | } 18 | ], 19 | "require": { 20 | "php": "^8.0", 21 | "illuminate/contracts": "^11.0|^12.0", 22 | "spatie/laravel-package-tools": "^1.9.2" 23 | }, 24 | "require-dev": { 25 | "friendsofphp/php-cs-fixer": "^3.8", 26 | "larastan/larastan": "^3.0", 27 | "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0", 28 | "pestphp/pest": "^1|^2|^3.7", 29 | "pestphp/pest-plugin-laravel": "^1.1|^2.0|^3.1", 30 | "phpstan/extension-installer": "^1.1", 31 | "phpstan/phpstan-deprecation-rules": "^1.0|^2.0", 32 | "phpstan/phpstan-phpunit": "^1.0|^2.0", 33 | "phpunit/phpunit": "^9.0|^10.0|^11.5.3", 34 | "rector/rector": "^2.0", 35 | "roave/security-advisories": "dev-latest", 36 | "spatie/invade": "^2.0", 37 | "spatie/pest-plugin-test-time": "^1.0|^2.0", 38 | "spatie/ray": "^1.37" 39 | }, 40 | "autoload": { 41 | "psr-4": { 42 | "Oddvalue\\LaravelDrafts\\": "src", 43 | "Oddvalue\\LaravelDrafts\\Database\\Factories\\": "database/factories" 44 | } 45 | }, 46 | "autoload-dev": { 47 | "psr-4": { 48 | "Oddvalue\\LaravelDrafts\\Tests\\": "tests" 49 | } 50 | }, 51 | "scripts": { 52 | "analyse": "vendor/bin/phpstan analyse", 53 | "test": "vendor/bin/pest", 54 | "test-coverage": "vendor/bin/pest --coverage", 55 | "format": "vendor/bin/php-cs-fixer fix --allow-risky=yes && vendor/bin/rector" 56 | }, 57 | "config": { 58 | "sort-packages": true, 59 | "allow-plugins": { 60 | "pestphp/pest-plugin": true, 61 | "phpstan/extension-installer": true 62 | } 63 | }, 64 | "extra": { 65 | "laravel": { 66 | "providers": [ 67 | "Oddvalue\\LaravelDrafts\\LaravelDraftsServiceProvider" 68 | ], 69 | "aliases": { 70 | "LaravelDrafts": "Oddvalue\\LaravelDrafts\\Facades\\LaravelDrafts" 71 | } 72 | } 73 | }, 74 | "minimum-stability": "dev", 75 | "prefer-stable": true 76 | } 77 | -------------------------------------------------------------------------------- /config/drafts.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'keep' => 10, 6 | ], 7 | 8 | 'column_names' => [ 9 | /* 10 | * Boolean column that marks a row as the current version of the data for editing. 11 | */ 12 | 'is_current' => 'is_current', 13 | 14 | /* 15 | * Boolean column that marks a row as live and displayable to the public. 16 | */ 17 | 'is_published' => 'is_published', 18 | 19 | /* 20 | * Timestamp column that stores the date and time when the row was published. 21 | */ 22 | 'published_at' => 'published_at', 23 | 24 | /* 25 | * UUID column that stores the unique identifier of the model drafts. 26 | */ 27 | 'uuid' => 'uuid', 28 | 29 | /* 30 | * Name of the morph relationship to the publishing user. 31 | */ 32 | 'publisher_morph_name' => 'publisher', 33 | ], 34 | 35 | 'auth' => [ 36 | /* 37 | * The guard to fetch the logged-in user from for the publisher relation. 38 | */ 39 | 'guard' => 'web', 40 | ], 41 | ]; 42 | -------------------------------------------------------------------------------- /database/factories/ModelFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->sentence, 18 | 'is_current' => true, 19 | ]; 20 | } 21 | 22 | public function draft() 23 | { 24 | return $this->state(function () { 25 | return [ 26 | 'published_at' => null, 27 | 'is_published' => false, 28 | ]; 29 | }); 30 | } 31 | 32 | public function published() 33 | { 34 | return $this->state(function () { 35 | return [ 36 | 'published_at' => now()->toDateTimeString(), 37 | 'is_published' => true, 38 | ]; 39 | }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /database/factories/PostSectionFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->paragraph, 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /database/factories/SoftDeletingPostFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->sentence, 18 | ]; 19 | } 20 | 21 | public function draft() 22 | { 23 | return $this->state(function () { 24 | return [ 25 | 'published_at' => null, 26 | 'is_published' => false, 27 | ]; 28 | }); 29 | } 30 | 31 | public function published() 32 | { 33 | return $this->state(function () { 34 | return [ 35 | 'published_at' => now()->toDateTimeString(), 36 | 'is_published' => true, 37 | ]; 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /database/factories/TagFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->word(), 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | withPaths([ 9 | __DIR__ . '/config', 10 | __DIR__ . '/src', 11 | __DIR__ . '/tests', 12 | ]) 13 | ->withPhpSets() 14 | ->withPreparedSets( 15 | deadCode: true, 16 | codeQuality: true, 17 | codingStyle: true, 18 | typeDeclarations: true, 19 | privatization: true, 20 | earlyReturn: true, 21 | strictBooleans: true, 22 | ); 23 | -------------------------------------------------------------------------------- /resources/views/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oddvalue/laravel-drafts/8d37b9d38b4c9fbaf1f40e29ee04131d8d1983de/resources/views/.gitkeep -------------------------------------------------------------------------------- /src/Concerns/HasDrafts.php: -------------------------------------------------------------------------------- 1 | mergeCasts([ 39 | $this->getIsCurrentColumn() => 'boolean', 40 | $this->getIsPublishedColumn() => 'boolean', 41 | $this->getPublishedAtColumn() => 'datetime', 42 | ]); 43 | } 44 | 45 | public static function bootHasDrafts(): void 46 | { 47 | static::addGlobalScope('onlyCurrentInPreviewMode', static function (Builder $builder): void { 48 | if (LaravelDrafts::isPreviewModeEnabled()) { 49 | $builder->current(); 50 | } 51 | }); 52 | 53 | static::creating(function (Model $model): void { 54 | $model->{$model->getIsCurrentColumn()} = true; 55 | $model->setPublisher(); 56 | $model->generateUuid(); 57 | if ($model->{$model->getIsPublishedColumn()} !== false) { 58 | $model->publish(); 59 | } 60 | }); 61 | 62 | static::updating(function (Model $model): void { 63 | $model->newRevision(); 64 | }); 65 | 66 | static::publishing(function (Model $model): void { 67 | $model->setLive(); 68 | }); 69 | 70 | static::deleted(function (Model $model): void { 71 | $model->revisions()->delete(); 72 | }); 73 | 74 | if (method_exists(static::class, 'restored')) { 75 | static::restored(function (Model $model): void { 76 | $model->revisions()->restore(); 77 | }); 78 | } 79 | 80 | if (method_exists(static::class, 'forceDeleted')) { 81 | static::forceDeleted(function (Model $model): void { 82 | $model->revisions()->forceDelete(); 83 | }); 84 | } 85 | } 86 | 87 | protected function newRevision(): void 88 | { 89 | if ( 90 | // Revisions are disabled 91 | config('drafts.revisions.keep') < 1 92 | // This model has been set not to create a revision 93 | || $this->shouldCreateRevision() === false 94 | // The record is being soft deleted or restored 95 | || $this->isDirty(method_exists($this, 'getDeletedAtColumn') ? $this->getDeletedAtColumn() : 'deleted_at') 96 | // A listener of the creatingRevision event returned false 97 | || $this->fireModelEvent('creatingRevision') === false 98 | ) { 99 | return; 100 | } 101 | 102 | $updatingModel = $this->fresh(); 103 | $revision = $updatingModel?->replicate(); 104 | 105 | static::saved(function (Model $model) use ($updatingModel, $revision): void { 106 | if ($model->isNot($this)) { 107 | return; 108 | } 109 | 110 | $revision->{$this->getCreatedAtColumn()} = $updatingModel->{$this->getCreatedAtColumn()}; 111 | $revision->{$this->getUpdatedAtColumn()} = $updatingModel->{$this->getUpdatedAtColumn()}; 112 | $revision->is_current = false; 113 | $revision->is_published = false; 114 | 115 | $revision->saveQuietly(['timestamps' => false]); // Preserve the existing updated_at 116 | 117 | $this->setPublisher(); 118 | $this->pruneRevisions(); 119 | 120 | $this->fireModelEvent('createdRevision'); 121 | }); 122 | } 123 | 124 | public function withoutRevision(): static 125 | { 126 | $this->shouldCreateRevision = false; 127 | 128 | return $this; 129 | } 130 | 131 | public function shouldCreateRevision(): bool 132 | { 133 | return $this->shouldCreateRevision; 134 | } 135 | 136 | public function generateUuid(): void 137 | { 138 | if ($this->{$this->getUuidColumn()}) { 139 | return; 140 | } 141 | 142 | $this->{$this->getUuidColumn()} = Str::uuid(); 143 | } 144 | 145 | public function getDraftableAttributes(): array 146 | { 147 | return $this->getAttributes(); 148 | } 149 | 150 | public function setCurrent(): void 151 | { 152 | $this->{$this->getIsCurrentColumn()} = true; 153 | 154 | static::saved(function (Model $model): void { 155 | if ($model->isNot($this)) { 156 | return; 157 | } 158 | 159 | $this->revisions() 160 | ->withDrafts() 161 | ->current() 162 | ->excludeRevision($this) 163 | ->update([$this->getIsCurrentColumn() => false]); 164 | }); 165 | } 166 | 167 | public function setLive(): void 168 | { 169 | $published = $this->revisions()->published()->first(); 170 | 171 | if (! $published || $this->is($published)) { 172 | $this->{$this->getPublishedAtColumn()} ??= now(); 173 | $this->{$this->getIsPublishedColumn()} = true; 174 | $this->setCurrent(); 175 | 176 | return; 177 | } 178 | 179 | $oldAttributes = $published?->getDraftableAttributes() ?? []; 180 | $newAttributes = $this->getDraftableAttributes(); 181 | Arr::forget($oldAttributes, $this->getKeyName()); 182 | Arr::forget($newAttributes, $this->getKeyName()); 183 | 184 | $published->forceFill($newAttributes); 185 | $this->forceFill($oldAttributes); 186 | 187 | static::saved(function (Model $model) use ($published): void { 188 | if ($model->isNot($this)) { 189 | return; 190 | } 191 | 192 | $published->{$this->getIsPublishedColumn()} = true; 193 | $published->{$this->getPublishedAtColumn()} ??= now(); 194 | $published->setCurrent(); 195 | $published->saveQuietly(); 196 | 197 | $this->replicateAndAssociateDraftableRelations($published); 198 | }); 199 | 200 | $this->{$this->getIsPublishedColumn()} = false; 201 | $this->{$this->getPublishedAtColumn()} = null; 202 | $this->{$this->getIsCurrentColumn()} = false; 203 | $this->timestamps = false; 204 | $this->shouldCreateRevision = false; 205 | } 206 | 207 | public function replicateAndAssociateDraftableRelations(Model $published): void 208 | { 209 | collect($this->getDraftableRelations())->each(function (string $relationName) use ($published): void { 210 | $relation = $published->{$relationName}(); 211 | switch (true) { 212 | case $relation instanceof HasOne: 213 | if ($related = $this->{$relationName}) { 214 | $replicated = $related->replicate(); 215 | 216 | $method = method_exists($replicated, 'getDraftableAttributes') 217 | ? 'getDraftableAttributes' 218 | : 'getAttributes'; 219 | 220 | $published->{$relationName}()->create($replicated->$method()); 221 | } 222 | 223 | break; 224 | case $relation instanceof HasMany: 225 | $this->{$relationName}()->get()->each(function ($model) use ($published, $relationName): void { 226 | $replicated = $model->replicate(); 227 | 228 | $method = method_exists($replicated, 'getDraftableAttributes') 229 | ? 'getDraftableAttributes' 230 | : 'getAttributes'; 231 | 232 | $published->{$relationName}()->create($replicated->$method()); 233 | }); 234 | 235 | break; 236 | case $relation instanceof MorphToMany: 237 | case $relation instanceof BelongsToMany: 238 | $published->{$relationName}()->sync($this->{$relationName}()->pluck('id')); 239 | 240 | break; 241 | } 242 | }); 243 | } 244 | 245 | public function getDraftableRelations(): array 246 | { 247 | return property_exists($this, 'draftableRelations') ? $this->draftableRelations : []; 248 | } 249 | 250 | public function saveAsDraft(array $options = []): bool 251 | { 252 | if ($this->fireModelEvent('savingAsDraft') === false || $this->fireModelEvent('saving') === false) { 253 | return false; 254 | } 255 | 256 | $draft = $this->replicate(); 257 | $draft->{$this->getPublishedAtColumn()} = null; 258 | $draft->{$this->getIsPublishedColumn()} = false; 259 | $draft->shouldSaveAsDraft = false; 260 | $draft->setCurrent(); 261 | 262 | if ($saved = $draft->save($options)) { 263 | $this->fireModelEvent('drafted'); 264 | $this->pruneRevisions(); 265 | } 266 | 267 | return $saved; 268 | } 269 | 270 | public function asDraft(): static 271 | { 272 | $this->shouldSaveAsDraft = true; 273 | 274 | return $this; 275 | } 276 | 277 | public function shouldDraft(): bool 278 | { 279 | return $this->shouldSaveAsDraft; 280 | } 281 | 282 | public function setPublishedAttributes(): void 283 | { 284 | // Do nothing, everything should be handled by `setLive` 285 | } 286 | 287 | public function save(array $options = []): bool 288 | { 289 | if ( 290 | $this->exists 291 | && ( 292 | data_get($options, 'draft') || $this->shouldDraft() 293 | ) 294 | ) { 295 | return $this->saveAsDraft($options); 296 | } 297 | 298 | return parent::save($options); 299 | } 300 | 301 | public static function savingAsDraft(string|\Closure $callback): void 302 | { 303 | static::registerModelEvent('savingAsDraft', $callback); 304 | } 305 | 306 | public static function savedAsDraft(string|\Closure $callback): void 307 | { 308 | static::registerModelEvent('drafted', $callback); 309 | } 310 | 311 | public function updateAsDraft(array $attributes = [], array $options = []): bool 312 | { 313 | if (! $this->exists) { 314 | return false; 315 | } 316 | 317 | return $this->fill($attributes)->saveAsDraft($options); 318 | } 319 | 320 | public static function createDraft(...$attributes): self 321 | { 322 | return tap(static::make(...$attributes), function ($instance) { 323 | $instance->{$instance->getIsPublishedColumn()} = false; 324 | 325 | return $instance->save(); 326 | }); 327 | } 328 | 329 | public function setPublisher(): static 330 | { 331 | if ($this->{$this->getPublisherColumns()['id']} === null && LaravelDrafts::getCurrentUser()) { 332 | $this->publisher()->associate(LaravelDrafts::getCurrentUser()); 333 | } 334 | 335 | return $this; 336 | } 337 | 338 | public function pruneRevisions(): void 339 | { 340 | self::withoutEvents(function (): void { 341 | $revisionsToKeep = $this->revisions() 342 | ->orderByDesc($this->getUpdatedAtColumn()) 343 | ->onlyDrafts() 344 | ->withoutCurrent() 345 | ->take(config('drafts.revisions.keep')) 346 | ->pluck('id') 347 | ->merge($this->revisions()->current()->pluck('id')) 348 | ->merge($this->revisions()->published()->pluck('id')); 349 | 350 | $this->revisions() 351 | ->withDrafts() 352 | ->whereNotIn('id', $revisionsToKeep) 353 | ->delete(); 354 | }); 355 | } 356 | 357 | /** 358 | * Get the name of the "publisher" relation columns. 359 | */ 360 | #[ArrayShape(['id' => "string", 'type' => "string"])] 361 | public function getPublisherColumns(): array 362 | { 363 | return [ 364 | 'id' => defined(static::class.'::PUBLISHER_ID') 365 | ? static::PUBLISHER_ID 366 | : config('drafts.column_names.publisher_morph_name', 'publisher') . '_id', 367 | 'type' => defined(static::class.'::PUBLISHER_TYPE') 368 | ? static::PUBLISHER_TYPE 369 | : config('drafts.column_names.publisher_morph_name', 'publisher') . '_type', 370 | ]; 371 | } 372 | 373 | /** 374 | * Get the fully qualified "publisher" relation columns. 375 | */ 376 | public function getQualifiedPublisherColumns(): array 377 | { 378 | return array_map([$this, 'qualifyColumn'], $this->getPublisherColumns()); 379 | } 380 | 381 | public function getIsCurrentColumn(): string 382 | { 383 | return defined(static::class.'::IS_CURRENT') 384 | ? static::IS_CURRENT 385 | : config('drafts.column_names.is_current', 'is_current'); 386 | } 387 | 388 | public function getUuidColumn(): string 389 | { 390 | return defined(static::class . '::UUID') 391 | ? static::UUID 392 | : config('drafts.column_names.uuid', 'uuid'); 393 | } 394 | 395 | public function isCurrent(): bool 396 | { 397 | return $this->{$this->getIsCurrentColumn()} ?? false; 398 | } 399 | 400 | /* 401 | |-------------------------------------------------------------------------- 402 | | RELATIONS 403 | |-------------------------------------------------------------------------- 404 | */ 405 | 406 | public function revisions(): HasMany 407 | { 408 | return $this->hasMany(static::class, $this->getUuidColumn(), $this->getUuidColumn())->withDrafts(); 409 | } 410 | 411 | public function drafts() 412 | { 413 | return $this->revisions()->current()->onlyDrafts(); 414 | } 415 | 416 | public function publisher(): MorphTo 417 | { 418 | return $this->morphTo(config('drafts.column_names.publisher_morph_name')); 419 | } 420 | 421 | /* 422 | |-------------------------------------------------------------------------- 423 | | SCOPES 424 | |-------------------------------------------------------------------------- 425 | */ 426 | 427 | public function scopeCurrent(Builder $query): void 428 | { 429 | $query->withDrafts()->where($this->getIsCurrentColumn(), true); 430 | } 431 | 432 | public function scopeWithoutCurrent(Builder $query): void 433 | { 434 | $query->where($this->getIsCurrentColumn(), false); 435 | } 436 | 437 | public function scopeExcludeRevision(Builder $query, int | Model $exclude): void 438 | { 439 | $query->where($this->getKeyName(), '!=', is_int($exclude) ? $exclude : $exclude->getKey()); 440 | } 441 | 442 | /** 443 | * @deprecated This doesn't actually work, will be removed in next version 444 | */ 445 | public function scopeWithoutSelf(Builder $query): void 446 | { 447 | $query->where('id', '!=', $this->id); 448 | } 449 | 450 | /* 451 | |-------------------------------------------------------------------------- 452 | | ACCESSORS 453 | |-------------------------------------------------------------------------- 454 | */ 455 | 456 | public function getDraftAttribute() 457 | { 458 | if ($this->relationLoaded('drafts')) { 459 | return $this->drafts->first(); 460 | } 461 | 462 | if ($this->relationLoaded('revisions')) { 463 | return $this->revisions->firstWhere($this->getIsCurrentColumn(), true); 464 | } 465 | 466 | return $this->drafts()->first(); 467 | } 468 | 469 | /* 470 | |-------------------------------------------------------------------------- 471 | | MUTATORS 472 | |-------------------------------------------------------------------------- 473 | */ 474 | } 475 | -------------------------------------------------------------------------------- /src/Concerns/Publishes.php: -------------------------------------------------------------------------------- 1 | mergeCasts([ 29 | $this->getPublishedAtColumn() => 'datetime', 30 | $this->getIsPublishedColumn() => 'boolean', 31 | ]); 32 | } 33 | 34 | /** 35 | * Publish a model instance. 36 | */ 37 | public function publish(): static 38 | { 39 | if ($this->fireModelEvent('publishing') === false) { 40 | return $this; 41 | } 42 | 43 | $this->setPublishedAttributes(); 44 | 45 | static::saved(function (Model $model): void { 46 | if ($model->isNot($this)) { 47 | return; 48 | } 49 | 50 | $this->fireModelEvent('published'); 51 | }); 52 | 53 | return $this; 54 | } 55 | 56 | protected function setPublishedAttributes(): void 57 | { 58 | $this->{$this->getPublishedAtColumn()} ??= now(); 59 | $this->{$this->getIsPublishedColumn()} = true; 60 | } 61 | 62 | /** 63 | * Determine if the model instance has been published. 64 | */ 65 | public function isPublished(): bool 66 | { 67 | return $this->{$this->getIsPublishedColumn()} ?? false; 68 | } 69 | 70 | /** 71 | * Register a "published" model event callback with the dispatcher. 72 | */ 73 | public static function publishing(string|\Closure $callback): void 74 | { 75 | static::registerModelEvent('publishing', $callback); 76 | } 77 | 78 | /** 79 | * Register a "softDeleted" model event callback with the dispatcher. 80 | */ 81 | public static function published(string|\Closure $callback): void 82 | { 83 | static::registerModelEvent('published', $callback); 84 | } 85 | 86 | /** 87 | * Get the name of the "published at" column. 88 | */ 89 | public function getPublishedAtColumn(): string 90 | { 91 | return defined(static::class.'::PUBLISHED_AT') 92 | ? static::PUBLISHED_AT 93 | : config('drafts.column_names.published_at', 'published_at'); 94 | } 95 | 96 | /** 97 | * Get the fully qualified "published at" column. 98 | */ 99 | public function getQualifiedPublishedAtColumn(): string 100 | { 101 | return $this->qualifyColumn($this->getPublishedAtColumn()); 102 | } 103 | 104 | /** 105 | * Get the name of the "published at" column. 106 | */ 107 | public function getIsPublishedColumn(): string 108 | { 109 | return defined(static::class.'::IS_PUBLISHED') 110 | ? static::IS_PUBLISHED 111 | : config('drafts.column_names.is_published', 'is_published'); 112 | } 113 | 114 | /** 115 | * Get the fully qualified "published at" column. 116 | */ 117 | public function getQualifiedIsPublishedColumn(): string 118 | { 119 | return $this->qualifyColumn($this->getIsPublishedColumn()); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Facades/LaravelDrafts.php: -------------------------------------------------------------------------------- 1 | user(); 16 | } 17 | 18 | public function previewMode(bool $previewMode = true): void 19 | { 20 | Session::put('drafts.preview', $previewMode); 21 | } 22 | 23 | public function disablePreviewMode(): void 24 | { 25 | Session::forget('drafts.preview'); 26 | } 27 | 28 | public function isPreviewModeEnabled(): bool 29 | { 30 | return Session::get('drafts.preview', false); 31 | } 32 | 33 | public function withDrafts(bool $withDrafts = true): void 34 | { 35 | $this->withDrafts = $withDrafts; 36 | } 37 | 38 | public function isWithDraftsEnabled(): bool 39 | { 40 | return $this->withDrafts; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/LaravelDraftsServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('laravel-drafts') 23 | ->hasConfigFile() 24 | ->hasViews(); 25 | } 26 | 27 | public function packageRegistered(): void 28 | { 29 | $this->app->singleton(LaravelDrafts::class, fn (): LaravelDrafts => new LaravelDrafts()); 30 | 31 | $this->app[Kernel::class]->prependToMiddlewarePriority(WithDraftsMiddleware::class); 32 | 33 | Blueprint::macro('drafts', function ( 34 | ?string $uuid = null, 35 | ?string $publishedAt = null, 36 | ?string $isPublished = null, 37 | ?string $isCurrent = null, 38 | ?string $publisherMorphName = null, 39 | ): void { 40 | $uuid ??= config('drafts.column_names.uuid', 'uuid'); 41 | $publishedAt ??= config('drafts.column_names.published_at', 'published_at'); 42 | $isPublished ??= config('drafts.column_names.is_published', 'is_published'); 43 | $isCurrent ??= config('drafts.column_names.is_current', 'is_current'); 44 | $publisherMorphName ??= config('drafts.column_names.publisher_morph_name', 'publisher_morph_name'); 45 | 46 | $this->uuid($uuid)->nullable(); 47 | $this->timestamp($publishedAt)->nullable(); 48 | $this->boolean($isPublished)->default(false); 49 | $this->boolean($isCurrent)->default(false); 50 | $this->nullableMorphs($publisherMorphName); 51 | 52 | $this->index([$uuid, $isPublished, $isCurrent]); 53 | }); 54 | 55 | Blueprint::macro('dropDrafts', function ( 56 | ?string $uuid = null, 57 | ?string $publishedAt = null, 58 | ?string $isPublished = null, 59 | ?string $isCurrent = null, 60 | ?string $publisherMorphName = null, 61 | ): void { 62 | $uuid ??= config('drafts.column_names.uuid', 'uuid'); 63 | $publishedAt ??= config('drafts.column_names.published_at', 'published_at'); 64 | $isPublished ??= config('drafts.column_names.is_published', 'is_published'); 65 | $isCurrent ??= config('drafts.column_names.is_current', 'is_current'); 66 | $publisherMorphName ??= config('drafts.column_names.publisher_morph_name', 'publisher_morph_name'); 67 | 68 | $this->dropIndex([$uuid, $isPublished, $isCurrent]); 69 | $this->dropMorphs($publisherMorphName); 70 | 71 | $this->dropColumn([ 72 | $uuid, 73 | $publishedAt, 74 | $isPublished, 75 | $isCurrent, 76 | ]); 77 | }); 78 | 79 | Route::macro('withDrafts', function (\Closure $routes): void { 80 | Route::middleware(WithDraftsMiddleware::class)->group($routes); 81 | }); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Scopes/PublishingScope.php: -------------------------------------------------------------------------------- 1 | where($model->getQualifiedIsPublishedColumn(), 1); 26 | } 27 | 28 | public function extend(Builder $builder): void 29 | { 30 | foreach ($this->extensions as $extension) { 31 | $this->{'add' . $extension}($builder); 32 | } 33 | } 34 | 35 | // protected function addPublish(Builder $builder): void 36 | // { 37 | // $builder->macro('publish', function (Builder $builder) { 38 | // $builder->withDrafts(); 39 | // 40 | // return $builder->update([$builder->getModel()->getIsPublishedColumn() => now()]); 41 | // }); 42 | // } 43 | // 44 | // protected function addUnpublish(Builder $builder): void 45 | // { 46 | // $builder->macro('unpublish', function (Builder $builder) { 47 | // return $builder->update([$builder->getModel()->getIsPublishedColumn() => null]); 48 | // }); 49 | // } 50 | // 51 | // protected function addSchedule(Builder $builder): void 52 | // { 53 | // $builder->macro('schedule', function (Builder $builder, string | \DateTimeInterface $date) { 54 | // $builder->withDrafts(); 55 | // 56 | // return $builder->update([$builder->getModel()->getIsPublishedColumn() => $date]); 57 | // }); 58 | // } 59 | 60 | protected function addPublished(Builder $builder): void 61 | { 62 | $builder->macro( 63 | 'published', 64 | fn (Builder $builder, $withoutDrafts = true) => $builder->withDrafts(! $withoutDrafts), 65 | ); 66 | } 67 | 68 | protected function addWithDrafts(Builder $builder): void 69 | { 70 | $builder->macro('withDrafts', function (Builder $builder, $withDrafts = true) { 71 | if (! $withDrafts) { 72 | return $builder->withoutDrafts(); 73 | } 74 | 75 | return $builder->withoutGlobalScope($this); 76 | }); 77 | } 78 | 79 | protected function addWithoutDrafts(Builder $builder): void 80 | { 81 | $builder->macro('withoutDrafts', function (Builder $builder): Builder { 82 | $model = $builder->getModel(); 83 | 84 | $builder->withoutGlobalScope($this) 85 | ->where($model->getQualifiedIsPublishedColumn(), 1); 86 | 87 | return $builder; 88 | }); 89 | } 90 | 91 | protected function addOnlyDrafts(Builder $builder): void 92 | { 93 | $builder->macro('onlyDrafts', function (Builder $builder): Builder { 94 | $model = $builder->getModel(); 95 | 96 | $builder->withoutGlobalScope($this) 97 | ->where($model->getQualifiedIsPublishedColumn(), 0); 98 | 99 | return $builder; 100 | }); 101 | } 102 | } 103 | --------------------------------------------------------------------------------