diff --git a/content/6.laravel/10.Collections-&-Arrays.md b/content/6.laravel/1.Collections-&-Arrays.md similarity index 100% rename from content/6.laravel/10.Collections-&-Arrays.md rename to content/6.laravel/1.Collections-&-Arrays.md diff --git a/content/6.laravel/2.laravel-naming-conventions.md b/content/6.laravel/2.laravel-naming-conventions.md new file mode 100644 index 00000000..88edfb6a --- /dev/null +++ b/content/6.laravel/2.laravel-naming-conventions.md @@ -0,0 +1,210 @@ +--- +title: Laravel naming conventions +--- + +## Naming Controllers + +Controllers should be in PascalCase/CapitalCase. + +They should be in singular case, no spacing between words, and end with `"Controller"`. + +Also, each word should be capitalised (i.e. `BlogController`, not `blogcontroller`). + + +>👍 For example: `BlogController`, `AuthController`, `UserController`. + + +> ! Bad examples: UsersController (because it is in plural), Users (because it is missing the Controller suffix). + +## Naming database tables in Laravel + +DB tables should be in lower case, with underscores to separate words (snake_case), and should be in plural form. + +>👍 For example: `posts`, `project_tasks`, `uploaded_images`. + + +>! Bad examples: `all_posts`, `Posts`, `post`, `blogPosts` + + +> ## Pivot tables + +Pivot tables should be all lower case, each model in alphabetical order, separated by an underscore (snake_case). + +>👍 For example: `post_user`, `task_user` etc. + +> ! Bad examples: `users_posts`, `UsersPosts`. + +> ## Table columns names + +Table column names should be in snake_case (underscores between words) - in lower case. You shouldn't reference the table name. + +>👍 For example: `post_body`, `id`, `created_at`. + +> ! Bad examples: `blog_post_created_at`, `forum_thread_title`, `threadTitle`. + +> ## Primary Key + +This should normally be `id`. + +> ## Foreign Keys + +Foreign keys should be the model name (singular), with '_id' appended to it (assuming the PK in the other table is 'id'). + +>👍 For example: `comment_id`, `user_id`. + +> ## Variables + +Normal variables should typically be in camelCase, with the first character lower case. + +>👍 For example: `$users` = ..., `$bannedUsers` = .... + +> ! Bad examples: `$all_banned_users` = ..., `$Users`=.... + +If the variable contains an array or collection of multiple items then the variable name should be in plural. Otherwise, it should be in singular form. + +## Naming Conventions for models + +> ## Naming Models in Laravel + +A model should be in PascalCase/CapitalCase. They should be in singular, no spacing between words, and capitalised. + +>👍 For example: `User` (`\App\User` or `\App\Models\User`, etc), `ForumThread`, `Comment`. + +>! Bad examples: Users, `ForumPosts`, `blogpost`, `blog_post`, `Blog_posts`. + +Generally, your models should be able to automatically work out what database table it should use from the following method: + +```php +table)) { + return str_replace( + '\\', '', Str::snake(Str::plural(class_basename($this))) + ); + } + return $this->table; + } +``` + +But of course you can just set `$this->table` in your model. See the section below on table naming conventions. + +I recommend that you create models and migrations at the same time by running` php artisan make:model -m ForumPost`. +This will auto-generate the migration file (in this case, for a DB table name of 'forum_posts'). + +> ## Model properties + +These should be lower case, snake_case. They should also follow the same conventions as the table column names. + +>👍 For example: `$this->updated_at`, `$this->title`. + +> ! Bad examples: `$this->UpdatedAt`, `$this->blogTitle`. + +> ## Model Methods + +Methods in your models in Laravel projects, like all methods in your Laravel projects, should be camelCase with the first character lower case + +>👍 For example: `public function get()`, `public function getAll()`. + +> ! Bad examples: `public function GetPosts()`, `public function get_posts()`. + +> ## Relationships + +### hasOne or belongsTo relationship (one to many) + +These should be singular form and follow the same naming conventions of normal model methods (camelCase, with the first letter lower case) + +>👍 For example: `public function postAuthor()`, `public function phone()`. + +### hasMany, belongsToMany, hasManyThrough (one to many) + +>👍 For example: `public function comments()`, `public function roles()`. + +### Polymorphic relationships + +These can be a bit awkward to get the naming correct. + +Ideally, you want to be able to have a method such as this: + +```php +morphMany('App\Category', 'categoryable'); +} +``` + +And Laravel will by default assume that there is a categoryable_id and categoryable_type. + +But you can use the other optional parameters for `morphMany ( public function morphMany($related, $name, $type = null, +$id = null, $localKey = null))` to change the defaults. + +## Controllers + +## Method naming conventions in controllers + +These should follow the same rules as model methods. I.e. camelCase (first character lowercase)These should follow the same rules as model methods. I.e. camelCase (first character lowercase). + +In addition, for normal CRUD operations, they should use one of the following method names. + +| VERB | URI | TYPICAL METHOD NAME | ROUTE NAME | +|------------|----------------------|---------------------|-----------------| +| GET | `/photos` | `index()` | `photos.index` | +| GET | `/photos/create` | `create()` | `photos.create` | +| POST | `/photos` | `store()` | `photos.store` | +| GET | `/photos/{photo}` | `show()` | `photos.show` | +| GET | `/photos/{photo}/edit` | `edit()` | `photos.edit` | +| PUT/PATCH | `/photos/{photo}` | `update()` | `photos.update` | +| DELETE | `/photos/{photo}` | `destroy()` | `photos.destroy` | + + +## Traits + +Traits should be be adjective words. + +>👍 For example: `Notifiable`, `Dispatchable`, etc. + +## Blade view files + +Blade files should be in lower case, snake_case (underscore between words). + +>👍 For example: `all.blade.php`, `all_posts.blade.php`, etc + +## Follow Laravel naming conventions + +Also, follow naming conventions accepted by Laravel community: + +| What | How | Good | Bad | +|--------------------------------|---------------------------------------|-------------------------------------------|--------------------------------------| +| Controller | singular | `ArticleController` | ~~ArticlesController~~ | +| Route | plural | `articles/1` | ~~article/1~~ | +| Route name | snake_case with dot notation | `users.show_active` | ~~users-show-active~~ | +| Model | singular | `User` | ~~Users~~ | +| hasOne or belongsTo relationship | singular | `articleComment` | ~~articleComments~~ | +| All other relationships | plural | `articleComments` | ~~articleComment~~ | +| Table | plural | `article_comments` | ~~article_comment~~ | +| Pivot table | singular model names in alphabetical order | `article_user` | ~~user_article~~ | +| Table column | snake_case without model name | `meta_title` | ~~article_meta_title~~ | +| Model property | snake_case | `$model->created_at` | ~~$model->CreatedAt~~ | +| Foreign key | singular model name + _id suffix | `article_id` | ~~articleId~~ | +| Primary key | singular | `id` | ~~article_id~~ | +| Migration | timestamp_create_table_name | `2017_01_01_000000_create_articles_table` | ~~2017-01-01-create-articles~~ | +| Method | camelCase | `getAll` | ~~get_all~~ | +| Method in resource controller | store | `store` | ~~saveArticle~~ | +| Method in test class | camelCase | `testGuestCannotSeeArticle` | ~~test_guest_cannot_see_article~~ | +| Variable | camelCase | `$articlesWithAuthor` | ~~$articles_with_author~~ | +| Collection | plural | `$activities = User::active()->get();` | ~~$activityList~~ | +| Object | singular | `$activeUser = User::active()->first();` | ~~$activeUsers~~ | +| Config and language files | snake_case | `articles_enabled` | ~~ArticlesEnabled~~ | +| View | kebab-case | `show-filtered.blade.php` | ~~showFiltered.blade.php~~ | +| Config | snake_case | `google_calendar.php` | ~~googleCalendar.php~~ | +| Contract (interface) | adjective + action | `AuthenticationInterface` | ~~AuthInterface~~ | +| Trait [(PSR)] | adjective | `NotifiableTrait` | ~~NotifierTrait~~ | +| FormRequest | singular | `UpdateUserRequest` | ~~UpdateUsersRequest~~ | +| Seeder | singular | `UserSeeder` | ~~UsersSeeder~~ | + diff --git a/content/6.laravel/3.laravel-best-practices.md b/content/6.laravel/3.laravel-best-practices.md new file mode 100644 index 00000000..994885be --- /dev/null +++ b/content/6.laravel/3.laravel-best-practices.md @@ -0,0 +1,638 @@ +--- +title: Laravel best Practices +--- + +## Single responsibility principle + +A class should have only one responsibility. + +> Bad: + +```php +public function update(Request $request): string +{ + $validated = $request->validate([ + 'title' => 'required|max:255', + 'events' => 'required|array:date,type' + ]); + + foreach ($request->events as $event) { + $date = $this->carbon->parse($event['date'])->toString(); + + $this->logger->log('Update event ' . $date . ' :: ' . $); + } + + $this->event->updateGeneralEvent($request->validated()); + + return back(); +} +``` + +> Good: + +```php +public function update(UpdateRequest $request): string +{ + $this->logService->logEvents($request->events); + + $this->event->updateGeneralEvent($request->validated()); + + return back(); +} +``` + +## Methods should do just one thing + +A function should do just one thing and do it well. + +> Bad: + +```php +public function getFullNameAttribute(): string +{ + if (auth()->user() && auth()->user()->hasRole('client') && auth()->user()->isVerified()) { + return 'Mr. ' . $this->first_name . ' ' . $this->middle_name . ' ' . $this->last_name; + } else { + return $this->first_name[0] . '. ' . $this->last_name; + } +} +``` + +> Good: + +```php +public function getFullNameAttribute(): string +{ + return $this->isVerifiedClient() ? $this->getFullNameLong() : $this->getFullNameShort(); +} + +public function isVerifiedClient(): bool +{ + return auth()->user() && auth()->user()->hasRole('client') && auth()->user()->isVerified(); +} + +public function getFullNameLong(): string +{ + return 'Mr. ' . $this->first_name . ' ' . $this->middle_name . ' ' . $this->last_name; +} + +public function getFullNameShort(): string +{ + return $this->first_name[0] . '. ' . $this->last_name; +} +``` + +## Fat models, skinny controllers + +Put all DB related logic into Eloquent models. + +> Bad: + +```php +public function index() +{ + $clients = Client::verified() + ->with(['orders' => function ($q) { + $q->where('created_at', '>', Carbon::today()->subWeek()); + }]) + ->get(); + + return view('index', ['clients' => $clients]); +} +``` + +> Good: + +```php +public function index() +{ + return view('index', ['clients' => $this->client->getWithNewOrders()]); +} + +class Client extends Model +{ + public function getWithNewOrders(): Collection + { + return $this->verified() + ->with(['orders' => function ($q) { + $q->where('created_at', '>', Carbon::today()->subWeek()); + }]) + ->get(); + } +} +``` + +## Validation + +Move validation from controllers to Request classes. + +> Bad: + +```php +public function store(Request $request) +{ + $request->validate([ + 'title' => 'required|unique:posts|max:255', + 'body' => 'required', + 'publish_at' => 'nullable|date', + ]); + + ... +} +``` + +> Good: + +```php +public function store(PostRequest $request) +{ + ... +} + +class PostRequest extends Request +{ + public function rules(): array + { + return [ + 'title' => 'required|unique:posts|max:255', + 'body' => 'required', + 'publish_at' => 'nullable|date', + ]; + } +} +``` + +## Business logic should be in service class + +A controller must have only one responsibility, so move business logic from controllers to service classes. + +> Bad: + +```php +public function store(Request $request) +{ + if ($request->hasFile('image')) { + $request->file('image')->move(public_path('images') . 'temp'); + } + + ... +} +``` + +> Good: + +```php +public function store(Request $request) +{ + $this->articleService->handleUploadedImage($request->file('image')); + + ... +} + +class ArticleService +{ + public function handleUploadedImage($image): void + { + if (!is_null($image)) { + $image->move(public_path('images') . 'temp'); + } + } +} +``` + +## Don't repeat yourself (DRY) + +Reuse code when you can. SRP is helping you to avoid duplication. Also, reuse Blade templates, use Eloquent scopes etc. + +> Bad: + +```php +public function getActive() +{ + return $this->where('verified', 1)->whereNotNull('deleted_at')->get(); +} + +public function getArticles() +{ + return $this->whereHas('user', function ($q) { + $q->where('verified', 1)->whereNotNull('deleted_at'); + })->get(); +} +``` + +> Good: + +```php +public function scopeActive($q) +{ + return $q->where('verified', true)->whereNotNull('deleted_at'); +} + +public function getActive(): Collection +{ + return $this->active()->get(); +} + +public function getArticles(): Collection +{ + return $this->whereHas('user', function ($q) { + $q->active(); + })->get(); +} +``` + +## Prefer to use Eloquent over using Query Builder and raw SQL queries. Prefer collections over arrays + +Eloquent allows you to write readable and maintainable code. Also, Eloquent has great built-in tools like soft deletes, +events, scopes etc. You may want to check out [Eloquent to SQL reference](https://github.com/alexeymezenin/eloquent-sql-reference) + +> Bad: + +```php +SELECT * +FROM `articles` +WHERE EXISTS (SELECT * + FROM `users` + WHERE `articles`.`user_id` = `users`.`id` + AND EXISTS (SELECT * + FROM `profiles` + WHERE `profiles`.`user_id` = `users`.`id`) + AND `users`.`deleted_at` IS NULL) +AND `verified` = '1' +AND `active` = '1' +ORDER BY `created_at` DESC +``` +> Good: + +```php +Article::has('user.profile')->verified()->latest()->get(); +``` +## Mass assignment + +> Bad: + +```php +$article = new Article; +$article->title = $request->title; +$article->content = $request->content; +$article->verified = $request->verified; + +// Add category to article +$article->category_id = $category->id; +$article->save(); +``` + +> Good: + +```php +$category->article()->create($request->validated()); +``` + +## Do not execute queries in Blade templates and use eager loading (N + 1 problem) + +> Bad (for 100 users, 101 DB queries will be executed): + +```php +@foreach (User::all() as $user) + {{ $user->profile->name }} +@endforeach +``` + +> Good (for 100 users, 2 DB queries will be executed): + +```php +$users = User::with('profile')->get(); + +@foreach ($users as $user) + {{ $user->profile->name }} +@endforeach +``` + +## Chunk data for data-heavy tasks + +> Bad: + +```php +$users = $this->get(); + +foreach ($users as $user) { + ... +} +``` + +> Good: + +```php +$this->chunk(500, function ($users) { + foreach ($users as $user) { + ... + } +}); +``` + +## Prefer descriptive method and variable names over comments + +> Bad: + +```php +// Determine if there are any joins +if (count((array) $builder->getQuery()->joins) > 0) +``` +> Good: + +```php +if ($this->hasJoins()) +``` +## Do not put JS and CSS in Blade templates and do not put any HTML in PHP classes + +> Bad: + +```php +let article = `{{ json_encode($article) }}`; +``` + +> Better: + +```js + + +Or + +