├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── ping.json └── src ├── Exceptions └── CaseBuilderException.php ├── Facades └── CaseBuilder.php ├── LaravelCaseServiceProvider.php └── Query ├── CaseBuilder.php └── Grammar.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 3.1.0 2 | - Adding support for NULL case values 3 | - Adding support for dot-separation to specify table name and column name 4 | 5 | ## 3.0.0 6 | - Adding support for Laravel 11 7 | 8 | ## 2.0.0 9 | - Adding support for Laravel 10 10 | 11 | ## 1.4.0 12 | - Feature: Add `caseRaw` support 13 | 14 | ## 1.3.0 15 | 16 | - Bugfix: make the facade to always resolve a new instance of the CaseBuilder object. 17 | 18 | ## 1.2.0 19 | 20 | - Bugfix: create a new instance of the CaseBuilder in the SC bind. 21 | 22 | ## 1.1.0 23 | 24 | - Bugfix: fix cases where elseRaw is equal to 0. 25 | 26 | ## 1.0.0 27 | 28 | - Initial release. 29 | 30 | ## 0.0.1 31 | 32 | - Experimental initial release. 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) : Agli Pançi 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 | # Laravel Eloquent CASE Statement Support 2 | ![Test Status](https://img.shields.io/github/actions/workflow/status/aglipanci/laravel-eloquent-case/run-tests.yml?branch=main) 3 | 4 | This packages adds [CASE](https://dev.mysql.com/doc/refman/5.7/en/flow-control-functions.html#operator_case) statement support to Laravel Query Builder. It supports Laravel 8.x, 9.x, 10.x & 11.x. 5 | 6 | ## Usage 7 | 8 | ### Add a CASE statement select on a Laravel Query 9 | 10 | ```php 11 | use App\Models\Invoice; 12 | use AgliPanci\LaravelCase\Query\CaseBuilder; 13 | 14 | $invoices = Invoice::query() 15 | ->case(function (CaseBuilder $case) { 16 | $case->when('balance', '<', 0)->then('Overpaid') 17 | ->when('balance', 0)->then('Paid') 18 | ->else('Balance Due'); 19 | }, 'payment_status') 20 | ->get(); 21 | ``` 22 | 23 | Produces the following SQL query: 24 | 25 | ```mysql 26 | SELECT 27 | ( CASE 28 | WHEN `balance` < 0 THEN 'Overpaid' 29 | WHEN `balance` = 0 THEN 'Paid' 30 | ELSE 'Balance Due' 31 | END ) AS `payment_status` 32 | FROM 33 | `invoices` 34 | ``` 35 | 36 | ### Build the case query separately 37 | 38 | ```php 39 | use App\Models\Invoice; 40 | use AgliPanci\LaravelCase\Facades\CaseBuilder; 41 | 42 | $caseQuery = CaseBuilder::when('balance', 0)->then('Paid') 43 | ->when('balance', '>', 0)->then('Balance Due'); 44 | 45 | $invoices = Invoice::query() 46 | ->case($caseQuery, 'payment_status') 47 | ->get(); 48 | ``` 49 | 50 | ### Raw CASE conditions 51 | 52 | ```php 53 | use App\Models\Invoice; 54 | use AgliPanci\LaravelCase\Facades\CaseBuilder; 55 | 56 | $caseQuery = CaseBuilder::whenRaw('balance = ?', [0])->thenRaw("'Paid'") 57 | ->elseRaw("'N/A'") 58 | 59 | $invoices = Invoice::query() 60 | ->case($caseQuery, 'payment_status') 61 | ->get(); 62 | ``` 63 | 64 | ### Use as raw SELECT 65 | 66 | ```php 67 | use App\Models\Invoice; 68 | use \AgliPanci\LaravelCase\Facades\CaseBuilder; 69 | 70 | $caseQuery = CaseBuilder::whenRaw('balance = ?', [0])->thenRaw("'Paid'") 71 | ->elseRaw("'N/A'") 72 | 73 | $invoices = Invoice::query() 74 | ->selectRaw($caseQuery->toRaw()) 75 | ->get(); 76 | ``` 77 | 78 | ### Available methods 79 | 80 | ```php 81 | use AgliPanci\LaravelCase\Facades\CaseBuilder; 82 | 83 | $caseQuery = CaseBuilder::whenRaw('balance = ?', [0])->thenRaw("'Paid'") 84 | ->elseRaw("'N/A'"); 85 | 86 | // Get the SQL representation of the query. 87 | $caseQuery->toSql(); 88 | 89 | // Get the query bindings. 90 | $caseQuery->getBindings(); 91 | 92 | // Get the SQL representation of the query with bindings. 93 | $caseQuery->toRaw(); 94 | 95 | // Get an Illuminate\Database\Query\Builder instance. 96 | $caseQuery->toQuery(); 97 | ``` 98 | 99 | ## Installation 100 | 101 | You can install the package via composer: 102 | 103 | ```bash 104 | composer require aglipanci/laravel-eloquent-case 105 | ``` 106 | 107 | ### Testing 108 | 109 | ```bash 110 | composer test 111 | ``` 112 | 113 | ### Changelog 114 | 115 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 116 | 117 | ### Security 118 | 119 | If you discover any security related issues, please email agli.panci@gmail.com instead of using the issue tracker. 120 | 121 | ## Credits 122 | 123 | - [Agli Pançi](https://github.com/aglipanci) 124 | - [Eduard Lleshi](https://github.com/eduardlleshi) 125 | - [All Contributors](https://github.com/aglipanci/laravel-case/graphs/contributors) 126 | 127 | ## License 128 | 129 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 130 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aglipanci/laravel-eloquent-case", 3 | "description": "Adds CASE statement support to Laravel Query Builder.", 4 | "homepage": "https://github.com/aglipanci/laravel-case", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Agli Panci", 9 | "email": "agli.panci@gmail.com", 10 | "role": "Developer" 11 | }, 12 | { 13 | "name": "Eduard Lleshi", 14 | "email": "eduard.lleshi@gmail.com", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.0", 20 | "illuminate/database": "^9.0|^10|^11|^12.0", 21 | "illuminate/support": "^9.0|^10|^11|^12.0" 22 | }, 23 | "require-dev": { 24 | "nunomaduro/larastan": "^2.0", 25 | "orchestra/testbench": "^6.23.0|^7.0.0|^10.0", 26 | "phpunit/phpunit": "^9.3.9|^11.5.3" 27 | }, 28 | "autoload": { 29 | "psr-4": { 30 | "AgliPanci\\LaravelCase\\": "src/" 31 | } 32 | }, 33 | "autoload-dev": { 34 | "psr-4": { 35 | "AgliPanci\\LaravelCase\\Tests\\": "tests" 36 | } 37 | }, 38 | "scripts": { 39 | "analyse": "vendor/bin/phpstan analyse --ansi", 40 | "test": "vendor/bin/phpunit", 41 | "test-coverage": "vendor/bin/phpunit --coverage-text", 42 | "sniff": "vendor/bin/php-cs-fixer fix --verbose --dry-run --diff", 43 | "lint": "vendor/bin/php-cs-fixer fix --verbose --show-progress=dots" 44 | }, 45 | "config": { 46 | "sort-packages": true 47 | }, 48 | "extra": { 49 | "laravel": { 50 | "providers": [ 51 | "AgliPanci\\LaravelCase\\LaravelCaseServiceProvider" 52 | ], 53 | "aliases": { 54 | "CaseBuilder": "CaseBuilder" 55 | } 56 | } 57 | }, 58 | "minimum-stability": "dev", 59 | "prefer-stable": true 60 | } 61 | -------------------------------------------------------------------------------- /ping.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "psr12" 3 | } -------------------------------------------------------------------------------- /src/Exceptions/CaseBuilderException.php: -------------------------------------------------------------------------------- 1 | selectRaw( 29 | '('.$caseBuilder->toSql().') as '.$this->grammar->wrap($as), 30 | $caseBuilder->getBindings() 31 | ); 32 | 33 | return $this; 34 | }); 35 | 36 | $this->app->bind( 37 | CaseBuilder::class, 38 | fn ($app) => new CaseBuilder($app->make(Builder::class), new Grammar) 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Query/CaseBuilder.php: -------------------------------------------------------------------------------- 1 | [], 24 | 'then' => [], 25 | 'else' => [], 26 | ]; 27 | 28 | public bool $sum = false; 29 | 30 | public Grammar $grammar; 31 | 32 | public QueryBuilder $queryBuilder; 33 | 34 | public function __construct( 35 | QueryBuilder $queryBuilder, 36 | Grammar $grammar 37 | ) { 38 | $this->queryBuilder = $queryBuilder; 39 | $this->grammar = $grammar; 40 | } 41 | 42 | public function case($subject): self 43 | { 44 | $this->subject = $this->grammar->wrapColumn($subject); 45 | 46 | return $this; 47 | } 48 | 49 | public function caseRaw($subject): self 50 | { 51 | $this->subject = $subject; 52 | 53 | return $this; 54 | } 55 | 56 | /** 57 | * @param mixed $column 58 | * @param mixed $operator 59 | * @param mixed $value 60 | * @return $this 61 | * 62 | * @throws Throwable 63 | */ 64 | public function when($column, $operator = null, $value = null): self 65 | { 66 | throw_if( 67 | ! $this->subject && func_num_args() === 1, 68 | CaseBuilderException::subjectMustBePresentWhenCaseOperatorNotUsed() 69 | ); 70 | 71 | throw_unless( 72 | count($this->whens) === count($this->thens), 73 | CaseBuilderException::wrongWhenPosition() 74 | ); 75 | 76 | [$value, $operator] = $this->queryBuilder->prepareValueAndOperator( 77 | $value, 78 | $operator, 79 | func_num_args() === 2 80 | ); 81 | 82 | if (isset($value)) { 83 | $this->addBinding($value, 'when'); 84 | 85 | $this->whens[] = [ 86 | 'query' => $this->grammar->wrapColumn($column).' '.$operator.' ?', 87 | 'binding' => count($this->bindings['when']) - 1, 88 | ]; 89 | } elseif (is_null($value)) { 90 | $operator = $operator === '=' ? 'IS' : 'IS NOT'; 91 | 92 | $this->whens[] = [ 93 | 'query' => $this->grammar->wrapColumn($column).' '.$operator.' NULL', 94 | ]; 95 | } elseif ($operator) { 96 | $this->addBinding($operator, 'when'); 97 | 98 | $this->whens[] = [ 99 | 'query' => $this->grammar->wrapColumn($column).' ?', 100 | 'binding' => count($this->bindings['when']) - 1, 101 | ]; 102 | } else { 103 | $this->whens[] = [ 104 | 'query' => $column, 105 | ]; 106 | } 107 | 108 | return $this; 109 | } 110 | 111 | /** 112 | * @throws Throwable 113 | */ 114 | public function whenRaw(string $expression, $bindings = []): self 115 | { 116 | throw_unless( 117 | count($this->whens) === count($this->thens), 118 | CaseBuilderException::wrongWhenPosition() 119 | ); 120 | 121 | $this->addBinding($bindings, 'when'); 122 | 123 | $this->whens[] = [ 124 | 'query' => $expression, 125 | 'binding' => count($this->bindings['when']) - 1, 126 | ]; 127 | 128 | return $this; 129 | } 130 | 131 | /** 132 | * @throws Throwable 133 | */ 134 | public function then($value): self 135 | { 136 | throw_if( 137 | count($this->whens) == count($this->thens), 138 | CaseBuilderException::thenCannotBeBeforeWhen() 139 | ); 140 | 141 | $this->addBinding($value, 'then'); 142 | 143 | $this->thens[] = '?'; 144 | 145 | return $this; 146 | } 147 | 148 | /** 149 | * @throws Throwable 150 | */ 151 | public function thenRaw($value, $bindings = []): self 152 | { 153 | throw_if( 154 | count($this->whens) == count($this->thens), 155 | CaseBuilderException::thenCannotBeBeforeWhen() 156 | ); 157 | 158 | $this->thens[] = $value; 159 | 160 | $this->addBinding($bindings, 'then'); 161 | 162 | return $this; 163 | } 164 | 165 | /** 166 | * @throws Throwable 167 | */ 168 | public function else($value): self 169 | { 170 | throw_if( 171 | $this->else, 172 | CaseBuilderException::elseIsPresent() 173 | ); 174 | 175 | throw_if( 176 | count($this->whens) === 0 || count($this->whens) !== count($this->thens), 177 | CaseBuilderException::elseCanOnlyBeAfterAWhenThen() 178 | ); 179 | 180 | $this->else = '?'; 181 | 182 | $this->addBinding($value, 'else'); 183 | 184 | return $this; 185 | } 186 | 187 | /** 188 | * @throws Throwable 189 | */ 190 | public function elseRaw($value, $bindings = []): self 191 | { 192 | throw_if( 193 | count($this->whens) === 0, 194 | CaseBuilderException::elseCanOnlyBeAfterAWhenThen() 195 | ); 196 | 197 | $this->else = $value; 198 | 199 | $this->addBinding($bindings, 'else'); 200 | 201 | return $this; 202 | } 203 | 204 | public function sum(): self 205 | { 206 | $this->sum = true; 207 | 208 | return $this; 209 | } 210 | 211 | /** 212 | * @throws Throwable 213 | */ 214 | public function toSql(): string 215 | { 216 | throw_if( 217 | ! count($this->whens) || ! count($this->thens), 218 | CaseBuilderException::noConditionsPresent() 219 | ); 220 | 221 | throw_if( 222 | count($this->whens) !== count($this->thens), 223 | CaseBuilderException::numberOfConditionsNotMatching() 224 | ); 225 | 226 | return $this->grammar->compile($this); 227 | } 228 | 229 | /** 230 | * @throws Throwable 231 | */ 232 | public function toRaw(): string 233 | { 234 | $bindings = array_map( 235 | fn ($parameter) => is_string($parameter) ? $this->grammar->wrapValue($parameter) : $parameter, 236 | $this->getBindings() 237 | ); 238 | 239 | return Str::replaceArray( 240 | '?', 241 | $bindings, 242 | $this->toSql() 243 | ); 244 | } 245 | 246 | /** 247 | * @param mixed $value 248 | * @return $this 249 | * 250 | * @throws \Throwable 251 | */ 252 | public function addBinding($value, string $type): CaseBuilder 253 | { 254 | throw_unless( 255 | array_key_exists($type, $this->bindings), 256 | InvalidArgumentException::class, 257 | "Invalid binding type: {$type}." 258 | ); 259 | 260 | $this->bindings[$type][] = $value; 261 | 262 | return $this; 263 | } 264 | 265 | public function getBindings(): array 266 | { 267 | $bindings = []; 268 | 269 | /** 270 | * Flattening here is to handle raw cases with multiple bindings. 271 | */ 272 | foreach ($this->whens as $i => $when) { 273 | if (array_key_exists('binding', $when)) { 274 | if (is_array($this->bindings['when'][$when['binding']])) { 275 | $bindings = array_merge($bindings, $this->bindings['when'][$when['binding']]); 276 | } else { 277 | $bindings[] = $this->bindings['when'][$when['binding']]; 278 | } 279 | } 280 | 281 | if (is_array($this->bindings['then'][$i])) { 282 | $bindings = array_merge($bindings, $this->bindings['then'][$i]); 283 | } else { 284 | $bindings[] = $this->bindings['then'][$i]; 285 | } 286 | } 287 | 288 | return array_merge($bindings, Arr::flatten($this->bindings['else'])); 289 | } 290 | 291 | /** 292 | * @throws Throwable 293 | */ 294 | public function toQuery(): QueryBuilder 295 | { 296 | return $this->queryBuilder->newQuery()->selectRaw($this->toSql(), $this->getBindings()); 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /src/Query/Grammar.php: -------------------------------------------------------------------------------- 1 | subject) { 15 | $components[] = $caseBuilder->subject; 16 | } 17 | 18 | foreach ($caseBuilder->whens as $i => $when) { 19 | $components[] = 'when'; 20 | $components[] = $when['query']; 21 | $components[] = 'then'; 22 | $components[] = $caseBuilder->thens[$i]; 23 | } 24 | 25 | if ($caseBuilder->else !== null) { 26 | $components[] = 'else'; 27 | $components[] = $caseBuilder->else; 28 | } 29 | 30 | $components[] = 'end'; 31 | 32 | $sql = trim(implode(' ', $components)); 33 | 34 | if ($caseBuilder->sum) { 35 | $sql = 'sum('.$sql.')'; 36 | } 37 | 38 | return $sql; 39 | } 40 | 41 | public function wrapColumn($value): string 42 | { 43 | $values = explode('.', $value); 44 | 45 | if (count($values) === 2) { 46 | return '`'.str_replace('`', '``', $values[0]).'`.'.'`'.str_replace('`', '``', $values[1]).'`'; 47 | } 48 | 49 | return '`'.str_replace('`', '``', $value).'`'; 50 | } 51 | 52 | public function wrapValue($value): string 53 | { 54 | return '"'.str_replace('"', '""', $value).'"'; 55 | } 56 | } 57 | --------------------------------------------------------------------------------