├── ping.json ├── src ├── Facades │ └── CaseBuilder.php ├── LaravelCaseServiceProvider.php ├── Query │ ├── Grammar.php │ └── CaseBuilder.php └── Exceptions │ └── CaseBuilderException.php ├── CHANGELOG.md ├── LICENSE.md ├── composer.json └── README.md /ping.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "psr12" 3 | } -------------------------------------------------------------------------------- /src/Facades/CaseBuilder.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/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 | -------------------------------------------------------------------------------- /src/Exceptions/CaseBuilderException.php: -------------------------------------------------------------------------------- 1 | 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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------