W tym artykule przyjrzymy się tym podstawowym najlepszym praktykom Laravel, od strukturyzacji kodu po optymalizację operacji na bazie danych, dzięki czemu Twoje projekty pozostaną wydajne i przyjazne dla programistów.
Niezależnie od tego, czy jesteś doświadczonym programistą Laravel, czy dopiero zaczynasz, te praktyki pomogą Ci podnieść poziom umiejętności programistycznych i dostarczać wysokiej jakości aplikacje.
Fat Models, Skinny Controllers
Przenieś logikę bazy danych na modele elokwentne, aby zachować czystsze kontrolery i kod
wielokrotnego użytkuZły przykład:
public function index()
{
$clients = Client::verified()
->with(['orders' => function ($query) {
$query->where('created_at', '>', now()->subDays(7));
}])
->get();
return view('index', compact('clients'));
}
Dobry przykład:
public function index(Client $client)
{
return view('index', ['clients' => $client->getVerifiedWithRecentOrders()]);
}
class Client extends Model
{
public function getVerifiedWithRecentOrders(): Collection
{
return $this->verified()
->with(['orders' => fn($query) => $query->recent()])
->get();
}
public function scopeVerified($query)
{
return $query->where('is_verified', true);
}
}
class Order extends Model
{
public function scopeRecent($query)
{
return $query->where('created_at', '>', now()->subDays(7));
}
}
Zasada
pojedynczej odpowiedzialności Klasa powinna mieć tylko jedną odpowiedzialność. Oznacza to, że klasa powinna skupiać się na jednym elemencie funkcjonalności. Naruszenie tej zasady może sprawić, że kod będzie trudniejszy do odczytania, przetestowania i utrzymania, ponieważ miesza problemy, które należy rozdzielić.
By adhering to the Zasada, you create code that is easier to understand and refactor. Each class or service has a clear purpose, making the overall system more modular and flexible.
wielokrotnego użytkuZły przykład:
public function update(Request $request): string
{
$validated = $request->validate([
'name' => 'required|max:255',
'tasks' => 'required|array:due_date,status'
]);
foreach ($request->tasks as $task) {
$formattedDate = $this->carbon->parse($task['due_date'])->toDateTimeString();
$this->logger->info('Task updated: ' . $formattedDate . ' - ' . $task['status']);
}
$this->project->updateTasks($request->validated());
return redirect()->route('projects.index');
}
Dobry przykład:
public function update(UpdateProjectRequest $request): string
{
$this->taskLogger->logTasks($request->tasks);
$this->projectService->updateTasks($request->validated());
return redirect()->route('projects.index');
}
class TaskLogger
{
public function logTasks(array $tasks): void
{
// Logic to log tasks
}
}
class ProjectService
{
public function updateTasks(array $data): void
{
// Logic to update project tasks
}
}
Metody powinny robić tylko jedną rzecz
: funkcja powinna mieć jeden cel i dobrze go wykonywać. Kiedy metoda robi więcej niż jedną rzecz, staje się trudniejsza do zrozumienia, przetestowania i utrzymania. Podział obowiązków na mniejsze, skoncentrowane metody sprawia, że kod jest bardziej czytelny i łatwiejszy do debugowania.
wielokrotnego użytkuZły przykład:
public function getFullNameAttribute(): string
{
if (auth()->user() && auth()->user()->hasRole('admin') && auth()->user()->isVerified()) {
return 'Admin ' . $this->first_name . ' ' . $this->last_name;
} else {
return $this->first_name[0] . '. ' . $this->last_name;
}
}
Dobry przykład:
public function getFullNameAttribute(): string
{
return $this->isVerifiedAdmin() ? $this->formatFullName() : $this->formatShortName();
}
private function isVerifiedAdmin(): bool
{
$user = auth()->user();
return $user && $user->hasRole('admin') && $user->isVerified();
}
private function formatFullName(): string
{
return 'Admin ' . $this->first_name . ' ' . $this->last_name;
}
private function formatShortName(): string
{
return strtoupper($this->first_name[0]) . '. ' . ucfirst($this->last_name);
}
Zachowaj logikę biznesową w klasach
usług Kontrolery powinny obsługiwać tylko żądania i odpowiedzi HTTP, delegując złożoną logikę do klas usług. Dzięki temu kod jest czysty, nadaje się do wielokrotnego użytku i jest łatwiejszy do przetestowania.
wielokrotnego użytkuZły przykład:
public function store(Request $request)
{
if ($request->hasFile('image')) {
$image = $request->file('image');
$image->storeAs('temp', $image->getClientOriginalName(), 'public');
}
// Other unrelated logic...
}
Dobry przykład:
public function store(Request $request, ArticleService $articleService)
{
$articleService->uploadImage($request->file('image'));
// Other unrelated logic...
}
class ArticleService
{
public function uploadImage(?UploadedFile $image): void
{
if ($image) {
$image->storeAs('uploads/temp', uniqid() . '_' . $image->getClientOriginalName(), 'public');
}
}
}
Unikaj logiki biznesowej w trasach
Trasy powinny obsługiwać tylko żądania HTTP, a nie logikę biznesową. Dzięki temu Twój kod jest czysty i łatwy w utrzymaniu.
Zły przykład
// Business logic in the route
Route::post('/article', function (Request $request) {
$article = new Article;
$article->title = $request->title;
$article->content = $request->content;
$article->save();
});
Dobry przykład
// Route delegates logic to the controller
Route::post('/article', [ArticleController::class, 'store']);
// In ArticleController
public function store(Request $request)
{
// logic to create article
}
Używanie relacji w celu ulepszenia kodu
Użyj relacji elokwentnych, aby uprościć i wyjaśnić, w jaki sposób powiązane modele współdziałają. Pozwala to uniknąć powtarzających się przypisań i sprawia, że kod jest łatwiejszy w utrzymaniu i mniej podatny na błędy.
Zły przykład
$article = new Article;
$article->title = $request->input('title');
$article->content = $request->input('content');
$article->verified = $request->boolean('verified');
$article->category_id = $category->id;
$article->save();
Dobry przykład
$category->articles()->create($request->safe()->only(['title', 'content', 'verified']));
Korzystanie z transakcji bazy danych dla niepodzielnych transakcji operacji
biznesowych zapewnia, że wszystkie operacje bazy danych kończą się powodzeniem lub niepowodzeniem jako grupa, zachowując integralność danych.
Zły przykład
public function placeOrder(Request $request)
{
$order = new Order;
$order->user_id = $request->user_id;
$order->save();
$payment = new Payment;
$payment->order_id = $order->id;
$payment->save();
}
Dobry przykład
use DB;
public function placeOrder(Request $request)
{
DB::beginTransaction();
try {
$order = Order::create($request->validated());
$payment = Payment::create(['order_id' => $order->id]);
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
}
Unikaj zapytań w bloku Blade: Użyj zachłannego ładowania
Wykonywanie zapytań wewnątrz szablonów bloku prowadzi do nieefektywnych wywołań bazy danych, zwłaszcza w przypadku pętli. Zachłanne ładowanie pobiera powiązane dane w jednym zapytaniu, zwiększając wydajność i unikając problemu z zapytaniami N + 1.
Zły przykład
@foreach (User::all() as $user)
{{ $user->profile->name }}
@endforeach
Jeśli masz 100 użytkowników, spowoduje to wyzwolenie 101 zapytań: jedno dla użytkowników i jedno dla każdego profilu użytkownika.
Dobry przykład
// in a service class or model passed back to controller which shares that with the blade file
$users = User::with('profile')->get();
// in blade file
@foreach ($users as $user)
{{ $user->profile->name }}
@endforeach
Spowoduje to wyzwolenie tylko 2 zapytań: jednego dla użytkowników i jednego dla ich profili.
Fragmentowanie danych w celu zwiększenia wydajności
W przypadku zadań, które obejmują duże zestawy danych, przetwarzanie we fragmentach zmniejsza użycie pamięci i poprawia wydajność, ograniczając ilość danych przechowywanych w pamięci jednocześnie.
Zły przykład
$users = User::all();
foreach ($users as $user) {
// Process each user
}
Dobry przykład
User::chunk(500, function ($users) {
foreach ($users as $user) {
// Process each user
}
});
Używaj stałych zamiast zakodowanych na stałe wartości
Korzystanie ze stałych pomoże Ci zlokalizować miejsca, w których ta wartość jest używana w przypadku, gdy chcesz ją zmienić, refaktoryzować i pomoże w debugowaniu.
Zły przykład
public function isAdmin(User $user): bool
{
return $user->type === 'admin';
}
Dobry przykład
public function isAdmin(User $user)
{
return $user->type === UserType::ADMIN;
}
Tłumaczenie ciągów
Podziękujesz sobie w przyszłości, gdy Twoja aplikacja będzie się rozwijać, jeśli od samego początku rozważyłeś tłumaczenie ciągów. Wszystko, czego potrzebujesz, aby przekazać ciągi __() znaków przez funkcję
Zły przykład
return back()->with('message', 'Your article has been added!');
Dobry przykład
return back()->with('message', __('Your article has been added!')); // notice the call to __()
Wstrzykiwanie zależności
Tworzenie instancji z klasami ściśle wiąże się z new klasami i utrudnia ich testowanie lub modyfikowanie. Korzystanie z kontenera IoC pozwala na łatwiejsze wstrzykiwanie zależności i lepszą testowalność.
Zły przykład
public function store(Request $request)
{
$user = new User;
$user->create($request->validated());
}
Dobry przykład
public function __construct(protected UserService $userService) {}
public function store(Request $request)
{
$this->userService->create($request->validated());
}
Unikaj bezpośredniego używania .env w kodzie
Uzyskiwanie dostępu do danych bezpośrednio z .env pliku w całej aplikacji może utrudnić utrzymanie i testowanie kodu. Zamiast tego przechowuj wartości w plikach konfiguracyjnych i pobieraj je za pomocą config().
Zły przykład
$apiKey = env('API_KEY');
Dobry przykład
// config/services.php
'api_key' => env('API_KEY'),
// Retrieve the value
$apiKey = config('services.api_key');
Przechowywanie dat jako obiektów, a nie ciągów
Przechowywanie dat jako ciągów może prowadzić do niespójnych formatów i błędów analizowania. Lepiej jest przechowywać je jako instancje Carbon, które zapewniają solidną obsługę dat. Dostępu i modyfikatorów można formatować daty tylko wtedy, gdy są potrzebne w warstwie wyświetlania.
Zły przykład
{{ Carbon::createFromFormat('Y-d-m H-i', $object->ordered_at)->toDateString() }}
{{ Carbon::createFromFormat('Y-d-m H-i', $object->ordered_at)->format('m-d') }}
Dobry przykład
// In Model
protected $casts = [
'ordered_at' => 'datetime',
];
// In Blade View
{{ $object->ordered_at->toDateString() }}
{{ $object->ordered_at->format('m-d') }}
Utrzymuj dokumentację kodu na minimalnym i znaczącym
poziomie Nadmierna dokumentacja często zaśmieca kod i utrudnia jego utrzymanie. Zamiast tego polegaj na jasnych, opisowych nazwach zmiennych, funkcji i klas. Używaj komentarzy tylko wtedy, gdy jest to absolutnie konieczne, aby wyjaśnić złożoną logikę.
Zły przykład
/**
* The function checks if the given string has any white spaces
*
* @param string $string String received from frontend which might contain
* space characters. Returns True if the string
* is valid.
*
* @return bool
*
* @license GPL
*/
public function checkString($string)
{
}
Dobry przykład
public function hasWhiteSpaces(string $string): bool
{
}
Dostosuj standardy kodowania do swojego zespołu
Spójny kod poprawia czytelność i łatwość konserwacji, ułatwiając współpracę.
Użyj puszki Laravel Pint , aby automatycznie formatować i egzekwować standardy kodowania. Integruje się z przepływem pracy programowania, dzięki czemu można go uruchamiać przed każdym zatwierdzeniem za pomocą punktów zaczepienia Git.
composer require --dev laravel/pint
vendor/bin/pint
Testowanie, testowanie i testowanie
I wreszcie, jedną z najważniejszych rzeczy, które możesz zrobić, aby zapewnić niezawodność, łatwość konserwacji i skalowalność swojego kodu, jest pisanie testów automatycznych.
Nie potrzebujesz 100% pokrycia swojej funkcjonalności (twierdzę, że przynosi to efekt przeciwny do zamierzonego), ale przynajmniej musisz upewnić się, że wszystkie trasy, które OTRZYMUJESZ, są pokryte, a im więcej możesz dodać na górze, tym lepiej.
Oto dlaczego:
-
Wczesne wykrywanie błędów: Testy pomagają zidentyfikować problemy, zanim trafią do produkcji. Wyłapywanie błędów podczas programowania jest znacznie tańsze i łatwiejsze niż naprawianie ich po wdrożeniu.
-
Pewność kodu: Dzięki odpowiedniemu pokryciu testami możesz bez obaw wprowadzać zmiany w bazie kodu. Testy zapewniają, że nowe zmiany nie przerywają istniejących funkcji.
-
Dokumentacja: Dobrze napisane testy działają jako żywa dokumentacja dla Twojego kodu. Opisują one, jak system ma się zachowywać i mogą służyć do zrozumienia intencji kodu.
-
Bezpieczna refaktoryzacja: Podczas refaktoryzacji lub ulepszania istniejącego kodu testy zapewniają siatkę bezpieczeństwa, która zapewnia, że żadna funkcjonalność nie zostanie utracona podczas zmian.
-
Ulepszony projekt: Pisanie testów często zachęca do lepszego projektowania oprogramowania. Aby napisać testowalny kod, zazwyczaj kończy się to mniejszymi, bardziej skoncentrowanymi metodami i klasami, które są łatwiejsze do utrzymania.
-
Współpraca: Testy ułatwiają zespołom wspólną pracę nad dużymi bazami kodu. Definiują jasne oczekiwania dotyczące zachowania, redukując nieporozumienia i poprawiając współpracę.
-
Ciągła integracja: Testy są niezbędne do wdrożenia przepływów pracy ciągłej integracji i dostarczania. Testy automatyczne mogą być uruchamiane przy każdym wypchnięciu kodu, zapewniając, że wdrażany jest tylko stabilny kod.
-
Utrzymanie długoterminowe: W dużych projektach testy pomagają utrzymać stabilność w czasie, zwłaszcza gdy zespół się zmienia. Nowi programiści mogą polegać na testach, aby zrozumieć zachowanie bazy kodu i upewnić się, że przyszłe zmiany nie spowodują przerwania funkcjonalności.
Nie przestawaj pchać, buduj dalej! 🤘