└── README.md /README.md: -------------------------------------------------------------------------------- 1 | # πŸ”₯ πŸš€ Laravel Eloquent Tips 2 | 3 | This is a shortlist of the amazing hidden Laravel eloquent 30 tips that make the code go on smoothly. 4 | 5 | ## 1 – Invisible Database Columns 6 | 7 | The invisible column is a new concept in MySQL 8. What it does: when you run a `select *` query it won't retrieve any invisible column. If you need an invisible column's value you have to specify it explicitly in the `select` statement. 8 | 9 | And now, Laravel supports these columns: 10 | 11 | ```php 12 | Schema::table('users', function (Blueprint $table){ 13 | $table->string('password')->invisble(); 14 | }); 15 | 16 | $user = User::first(); 17 | $user->secret == null; 18 | ``` 19 | 20 | --- 21 | 22 | ## 2 – saveQuietly 23 | 24 | If you ever need to save a model but you don't want to trigger any model events, you can use this method: 25 | 26 | ```php 27 | $user = User::first(); 28 | $user->name = "Hamid Afghan"; 29 | 30 | $user->saveQuietly(); 31 | ``` 32 | 33 | --- 34 | 35 | ## 3 – Default Attribute Values 36 | 37 | In Laravel, you can define default values for columns in two places: Migrations and models. 38 | 39 | ```php 40 | Schema::create('orders', function(Blueprint $table){ 41 | $table->bigIncrements('id'); 42 | $table->string('status', 20) 43 | ->nullable(false) 44 | ->default(App\Enums\OrderStatuses::DRAFT); 45 | }); 46 | ``` 47 | 48 | This is a well-known feature. The status column will have a default draft value. 49 | 50 | But what about this? 51 | 52 | ```php 53 | $order = new Order(); 54 | $order->status = null; 55 | ``` 56 | 57 | In this case, the status will be null, because it's not persisted yet. And sometimes it causes annoying null value bugs. But fortunately, you can specify default attribute values in the Model as well: 58 | 59 | ```php 60 | class Order extends Model 61 | { 62 | protected $attributes = [ 63 | 'status' => App\Enums\OrderStatuses::DRAFT, 64 | ]; 65 | } 66 | ``` 67 | 68 | And now the status will be draft for a New Order: 69 | 70 | ```php 71 | $order = new Order(); 72 | $order->status === 'draft'; 73 | ``` 74 | 75 | You can use these two approaches together and you'll never have a null value bug again. 76 | 77 | --- 78 | 79 | ## 4 – Attribute Cast 80 | 81 | Before Laravel 8. x we wrote attribute accessors and mutators like these: 82 | 83 | ```php 84 | class User extends Model{ 85 | public function getNameAttribute(string $value): string 86 | { 87 | return Str::upper($value); 88 | } 89 | 90 | public function setNameAttribute(string $value): string 91 | { 92 | $this->attributes['name'] = Str::lower($value); 93 | } 94 | } 95 | ``` 96 | 97 | It's not bad at all, but as Taylor says in the pull request: 98 | 99 | > This aspect of the framework has always felt a bit "dated" to me. To be honest, I think it's one of the least elegant parts of the framework that currently exists. First, it requires two methods. Second, the framework does not typically prefix methods that retrieve or set data on an object with get and set 100 | 101 | So he recreated this feature this way: 102 | 103 | ```php 104 | use Illuminate\Database\Eloquent\Casts\Attribute; 105 | 106 | class User extends Model { 107 | protected function name(): Attribute { 108 | return new Attribute( 109 | get: fn (string $value) => Str::upper($value), 110 | set: fn (string $value) => Str::lower($value) 111 | ); 112 | } 113 | } 114 | ``` 115 | 116 | The main differences: 117 | 118 | - You have to write only one method 119 | - It returns an Attribute instead of a scalar value 120 | - The Attribute itself takes a getter and a setter function 121 | 122 | In this example, I used PHP 8 named arguments (the get and set before the functions). 123 | 124 | --- 125 | 126 | ## 5 – find 127 | 128 | Everyone knows about the find method, but did you know that it accepts an array of IDs? So instead of this: 129 | 130 | ```php 131 | $users = User::whereIn('id', $ids)->get(); 132 | ``` 133 | 134 | You can use this: 135 | 136 | ```php 137 | $users = User::find($ids); 138 | ``` 139 | 140 | --- 141 | 142 | ## 6 – Get Dirty 143 | 144 | In Eloquent you can check if a model is "dirty" or not. Dirty means it has some changes that are not persisted yet: 145 | 146 | ```php 147 | $user = User::first(); 148 | $user->name = 'Hamid'; 149 | $user->isDirty(); // true 150 | $user->getDirty(); // ['name' => 'Hamid']; 151 | ``` 152 | 153 | The `isDirty` simply returns a bool while the `getDirty ` returns every dirty attribute. 154 | 155 | --- 156 | 157 | ## 7 – push 158 | 159 | Sometimes you need to save a model and its relationship as well. In this case, you can use the push method: 160 | 161 | ```php 162 | $employee = Employee::first(); 163 | $employee->name = 'New Name'; 164 | $employee->address->city = 'New York'; 165 | 166 | $employee->push(); 167 | ``` 168 | 169 | In this case, the, save would only save the name column in the employee's table but not the city column in the addresses table. The push method will save both. 170 | 171 | --- 172 | 173 | ## 8 – Boot Eloquent Traits 174 | 175 | We all write traits that are being used by Eloquent models. If you need to initialize something in your trait when an event happened in the model, you can boot your trait. 176 | 177 | For example, if you have models with slug, you don't want to rewrite the slug creation logic in every model. Instead, you can define a trait, and use the creating event in the boot method: 178 | 179 | ```php 180 | trait HasSlug { 181 | public static function bootHasSlug() { 182 | static::creating(function (Model $model) { 183 | $model->slug = Str::slug($model->title); 184 | }); 185 | } 186 | } 187 | ``` 188 | 189 | So you need to define a bootTraitName method, and Eloquent will automatically call this when it's booting a model. 190 | 191 | --- 192 | 193 | ## 9 – updateOrCreate 194 | 195 | Creating and updating a model often use the same logic. Fortunately Eloquent provides a very convenient method called updateOrCreate: 196 | 197 | ```php 198 | $flight = Flight::updateOrCreate( 199 | ['id' => $id], 200 | ['price' => 99, 'discounted' => 1], 201 | ); 202 | ``` 203 | 204 | It takes two arrays: 205 | 206 | - The first one is used to determine if the model exists or not. In this example, I use the id. 207 | - The second one is the attributes that you want to insert or update. 208 | 209 | And the way it works: 210 | 211 | - If a Flight is found based on the given id it will be updated with the second array. 212 | - If there's no Flight with the given id it will be inserted with the second array. 213 | 214 | I want to show you a real-world example of how I handle creating and updating models 215 | 216 | The Controller: 217 | 218 | ```php 219 | public function store(UpsertDepartmentRequest $request): JsonResponse { 220 | return DepartmentResource::make($this->upsert($request, new Department())) 221 | ->response() 222 | ->setStatusCode(Response::HTTP_CREATED); 223 | } 224 | 225 | 226 | public function update( UpsertDepartmentRequest $request, Department $department): HttpResponse { 227 | $this->upsert($request, $department); 228 | 229 | return response()->noContent(); 230 | } 231 | 232 | private function upsert(UpsertDepartmentRequest $request, Department $department): Department { 233 | 234 | $departmentData = new DepartmentData(...$request->validated()); 235 | 236 | return $this->upsertDepartment->execute($department, $departmentData); 237 | } 238 | ``` 239 | 240 | As you can see I often extract a method called upsert . This method accepts a Department . In the store method I use an empty Department instance because in this case, I don't have a real one. But in the 241 | 242 | update I pass the currently updated instance. 243 | 244 | The $this->upsertDepartment refers to an Action: 245 | 246 | ```php 247 | class UpsertDepartmentAction { 248 | 249 | public function execute( Department $department, DepartmentData $departmentData): Department { 250 | 251 | return Department->updateOrCreate( 252 | ['id' => $department->id],$departmentData->toArray() 253 | ); 254 | } 255 | } 256 | ``` 257 | 258 | It takes a Department which is the model (an empty one, or the updated one), and a DTO (a simple object that holds data). In the first array, I use the $department->id which is: 259 | 260 | - null if it's a new model. 261 | - A valid ID if it's an updated model. 262 | 263 | And the second argument is the DTO as an array, so the attributes of the Department. 264 | 265 | --- 266 | 267 | ## 10 – upsert 268 | 269 | Just for confusion Laravel uses the word upsert for multiple update or create operations. This is how it looks: 270 | 271 | ```php 272 | Flight::upsert( 273 | [ 274 | ['departure' => 'Oakland', 'destination' => 'San Diego', 'price' =>99], 275 | ['departure' => 'Chicago', 'destination' => 'New York', 'price' => 150] 276 | ], 277 | ['departure', 'destination'], 278 | ['price'] 279 | ); 280 | ``` 281 | 282 | It's a little bit more complicated: 283 | 284 | - First array: the values to insert or update 285 | - Second: unique identifier columns used in the select statement 286 | - Third: columns that you want to update if the record exists 287 | 288 | So this example will: 289 | 290 | - Insert or update a flight from Oakland to San Diego with the price of 99 291 | - Insert or update a flight from Chicago to New York with the price of 150 292 | 293 | --- 294 | 295 | ## 11 – Order by Mutator 296 | Imagine you have this: 297 | 298 | ```PHP 299 | function getFullNameAttribute() 300 | { 301 | return $this->attributes['first_name'] . ' ' . $this->attributes['last_name']; 302 | } 303 | ``` 304 | 305 | Now, you want to order by that full_name? This won’t work: 306 | 307 | ```PHP 308 | $clients = Client::orderBy('full_name')->get(); // doesn't work 309 | ``` 310 | 311 | The solution is quite simple. We need to order the results after we get them. 312 | 313 | ```PHP 314 | $clients = Client::get()->sortBy('full_name'); // works! 315 | ``` 316 | 317 | Notice that the function name is different – it’s not orderBy, it’s sortBy. 318 | 319 | **Note:** it is important to keep in your mind, if your query `Client::get()` returns a huge rows, `->sortBy()` function would require memory usege. Make sure the server don't go out of memorey. 320 | 321 | 322 | --- 323 | 324 | ## 12 – Raw query methods 325 | Eloquent statements may need the addition of raw queries. There are functions for it, however. 326 | 327 | ```PHP 328 | // WhereRaw 329 | $order = DB::table('orders') 330 | ->whereRaw('price < IF(state = "TX", ?, 100 )', [200]) 331 | ->get(); 332 | 333 | // havingRaw 334 | Product::groupBy('categrory_id') 335 | ->havingRaw('COUNT(*) > 1') 336 | ->get(); 337 | 338 | // orderbyRaw 339 | User::query() 340 | ->where('created_at', '>', $request->date) 341 | ->orderByRaw('(updated_at - created_at) DESC') 342 | ->get(); 343 | 344 | ``` 345 | 346 | ## 13 – whereColumn method 347 | The whereColumn method in Laravel's allows us to compare two columns from the same table. 348 | 349 | In this example, we are using it to retrieve all products where the price is less than or equal to the cost. 350 | 351 | ```PHP 352 | // WhereColumn 353 | $products = Product::whereColumn('price', '<=', 'cost')->get(); 354 | ``` 355 | 356 | ## 14 - appends 357 | 358 | If you have an attribute accessor and you often need it when the model is converted into JSON you can use the $appends property: 359 | 360 | ```php 361 | class Product extends Model { 362 | 363 | protected $appends = ['current_price']; 364 | 365 | public function getCurrentPriceAttribute(): float { 366 | 367 | return $this->prices 368 | ->where('from', '<=' now()) 369 | ->where('to', '>=', now()) 370 | ->first() 371 | ->price; 372 | } 373 | } 374 | ``` 375 | 376 | Now the `current_price` column will be appended to the Product model every time it gets converted into JSON. It's useful when you're working with Blade templates. With APIs, I would stick to Resources. 377 | --------------------------------------------------------------------------------