Laravel المتقدم
نمط طبقة الخدمة
فهم طبقة الخدمة
نمط طبقة الخدمة يغلف منطق الأعمال في فئات خدمة مخصصة، مما يحافظ على Controllers رفيعة ومركزة على معالجة طلبات HTTP. الخدمات تنسق بين المستودعات، وتنفذ عمليات معقدة، وتنفذ قواعد الأعمال.
فوائد طبقة الخدمة:
- فصل منطق الأعمال عن Controllers
- يجعل العمليات المعقدة قابلة لإعادة الاستخدام عبر Controllers
- يحسن قابلية الاختبار والصيانة للكود
- يوفر واجهة برمجة تطبيقات واضحة لعمليات الأعمال
- يجعل Controllers رفيعة ومركزة على شؤون HTTP
- يسهل إعادة استخدام الكود في Commands وJobs والسياقات الأخرى
فئة خدمة أساسية
لنُنشئ خدمة بسيطة للتعامل مع منطق الأعمال المتعلق بالمنشورات.
إنشاء خدمة أساسية:
// app/Services/PostService.php
namespace App\Services;
use App\Models\Post;
use App\Repositories\Contracts\PostRepositoryInterface;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
class PostService
{
protected $postRepository;
public function __construct(PostRepositoryInterface $postRepository)
{
$this->postRepository = $postRepository;
}
public function createPost(array $data): Post
{
// توليد slug من العنوان
$data['slug'] = Str::slug($data['title']);
// التأكد من أن slug فريد
$originalSlug = $data['slug'];
$counter = 1;
while ($this->postRepository->findBySlug($data['slug'])) {
$data['slug'] = $originalSlug . '-' . $counter++;
}
// تعيين المؤلف
$data['author_id'] = auth()->id();
// إنشاء المنشور
$post = $this->postRepository->create($data);
// مسح الذاكرة المؤقتة
Cache::forget('posts.recent');
Cache::forget('posts.featured');
return $post;
}
public function updatePost(int $id, array $data): ?Post
{
$post = $this->postRepository->find($id);
if (!$post) {
return null;
}
// تحديث slug إذا تغير العنوان
if (isset($data['title']) && $data['title'] !== $post->title) {
$data['slug'] = Str::slug($data['title']);
}
$this->postRepository->update($id, $data);
// مسح الذاكرة المؤقتة
Cache::forget("post.{$id}");
Cache::forget('posts.recent');
return $post->fresh();
}
public function publishPost(int $id): bool
{
$post = $this->postRepository->find($id);
if (!$post || $post->status === 'published') {
return false;
}
$this->postRepository->update($id, [
'status' => 'published',
'published_at' => now(),
]);
// إشعار المشتركين
// يمكن إرسال هذا إلى قائمة انتظار
// event(new PostPublished($post));
Cache::forget('posts.recent');
return true;
}
public function deletePost(int $id): bool
{
$post = $this->postRepository->find($id);
if (!$post) {
return false;
}
// التحقق من أن المستخدم لديه صلاحية
if ($post->author_id !== auth()->id() && !auth()->user()->isAdmin()) {
return false;
}
// حذف التعليقات المرتبطة
$post->comments()->delete();
// حذف المنشور
$this->postRepository->delete($id);
// مسح الذاكرة المؤقتة
Cache::forget("post.{$id}");
Cache::forget('posts.recent');
return true;
}
public function getFeaturedPosts(int $limit = 5)
{
return Cache::remember('posts.featured', 3600, function () use ($limit) {
return $this->postRepository
->published()
->where('is_featured', true)
->take($limit)
->get();
});
}
}
استخدام الخدمة في Controller:
// app/Http/Controllers/PostController.php
namespace App\Http\Controllers;
use App\Http\Requests\StorePostRequest;
use App\Http\Requests\UpdatePostRequest;
use App\Services\PostService;
use Illuminate\Http\Request;
class PostController extends Controller
{
protected $postService;
public function __construct(PostService $postService)
{
$this->postService = $postService;
}
public function store(StorePostRequest $request)
{
$post = $this->postService->createPost($request->validated());
return redirect()
->route('posts.show', $post->slug)
->with('success', 'Post created successfully');
}
public function update(UpdatePostRequest $request, int $id)
{
$post = $this->postService->updatePost($id, $request->validated());
if (!$post) {
return back()->with('error', 'Post not found');
}
return back()->with('success', 'Post updated successfully');
}
public function publish(int $id)
{
$published = $this->postService->publishPost($id);
if (!$published) {
return back()->with('error', 'Unable to publish post');
}
return back()->with('success', 'Post published successfully');
}
public function destroy(int $id)
{
$deleted = $this->postService->deletePost($id);
if (!$deleted) {
return back()->with('error', 'Unable to delete post');
}
return redirect()
->route('posts.index')
->with('success', 'Post deleted successfully');
}
}
كائنات نقل البيانات (DTOs)
DTOs هي كائنات بسيطة تحمل البيانات بين العمليات. تساعد في ضمان أمان النوع وتوفر عقداً واضحاً للبيانات المطلوبة.
إنشاء DTO:
// app/DTOs/CreatePostDTO.php
namespace App\DTOs;
class CreatePostDTO
{
public function __construct(
public readonly string $title,
public readonly string $content,
public readonly string $status,
public readonly ?string $excerpt = null,
public readonly ?int $categoryId = null,
public readonly array $tags = [],
public readonly bool $isFeatured = false,
) {}
public static function fromRequest(array $data): self
{
return new self(
title: $data['title'],
content: $data['content'],
status: $data['status'] ?? 'draft',
excerpt: $data['excerpt'] ?? null,
categoryId: $data['category_id'] ?? null,
tags: $data['tags'] ?? [],
isFeatured: $data['is_featured'] ?? false,
);
}
public function toArray(): array
{
return [
'title' => $this->title,
'content' => $this->content,
'status' => $this->status,
'excerpt' => $this->excerpt,
'category_id' => $this->categoryId,
'is_featured' => $this->isFeatured,
];
}
}
// app/DTOs/UpdatePostDTO.php
namespace App\DTOs;
class UpdatePostDTO
{
public function __construct(
public readonly ?string $title = null,
public readonly ?string $content = null,
public readonly ?string $status = null,
public readonly ?string $excerpt = null,
public readonly ?int $categoryId = null,
public readonly ?array $tags = null,
public readonly ?bool $isFeatured = null,
) {}
public static function fromRequest(array $data): self
{
return new self(
title: $data['title'] ?? null,
content: $data['content'] ?? null,
status: $data['status'] ?? null,
excerpt: $data['excerpt'] ?? null,
categoryId: $data['category_id'] ?? null,
tags: $data['tags'] ?? null,
isFeatured: $data['is_featured'] ?? null,
);
}
public function toArray(): array
{
return array_filter([
'title' => $this->title,
'content' => $this->content,
'status' => $this->status,
'excerpt' => $this->excerpt,
'category_id' => $this->categoryId,
'is_featured' => $this->isFeatured,
], fn($value) => $value !== null);
}
}
استخدام DTOs في الخدمة:
// app/Services/PostService.php (محسّن)
namespace App\Services;
use App\DTOs\CreatePostDTO;
use App\DTOs\UpdatePostDTO;
use App\Models\Post;
use App\Repositories\Contracts\PostRepositoryInterface;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
class PostService
{
protected $postRepository;
public function __construct(PostRepositoryInterface $postRepository)
{
$this->postRepository = $postRepository;
}
public function createPost(CreatePostDTO $dto): Post
{
$data = $dto->toArray();
// توليد slug فريد
$data['slug'] = $this->generateUniqueSlug($dto->title);
// تعيين المؤلف
$data['author_id'] = auth()->id();
// إنشاء المنشور
$post = $this->postRepository->create($data);
// مزامنة الوسوم
if (!empty($dto->tags)) {
$post->tags()->sync($dto->tags);
}
// مسح الذاكرة المؤقتة
$this->clearCache($post->id);
return $post;
}
public function updatePost(int $id, UpdatePostDTO $dto): ?Post
{
$post = $this->postRepository->find($id);
if (!$post) {
return null;
}
$data = $dto->toArray();
// تحديث slug إذا تغير العنوان
if (isset($data['title'])) {
$data['slug'] = $this->generateUniqueSlug($data['title'], $id);
}
$this->postRepository->update($id, $data);
// مزامنة الوسوم إذا تم توفيرها
if ($dto->tags !== null) {
$post->tags()->sync($dto->tags);
}
// مسح الذاكرة المؤقتة
$this->clearCache($id);
return $post->fresh();
}
protected function generateUniqueSlug(string $title, ?int $excludeId = null): string
{
$slug = Str::slug($title);
$originalSlug = $slug;
$counter = 1;
while ($this->slugExists($slug, $excludeId)) {
$slug = $originalSlug . '-' . $counter++;
}
return $slug;
}
protected function slugExists(string $slug, ?int $excludeId = null): bool
{
$query = Post::where('slug', $slug);
if ($excludeId) {
$query->where('id', '!=', $excludeId);
}
return $query->exists();
}
protected function clearCache(int $postId): void
{
Cache::forget("post.{$postId}");
Cache::forget('posts.recent');
Cache::forget('posts.featured');
}
}
فئات الإجراء (Action Classes)
فئات الإجراء هي فئات ذات مسؤولية واحدة تنفذ إجراءً محدداً واحداً. إنها مثالية للعمليات المعقدة التي لا تتناسب بشكل جيد مع الخدمات.
إنشاء فئات الإجراء:
// app/Actions/GeneratePostExcerptAction.php
namespace App\Actions;
use Illuminate\Support\Str;
class GeneratePostExcerptAction
{
public function execute(string $content, int $length = 200): string
{
// إزالة علامات HTML
$text = strip_tags($content);
// إزالة المسافات الزائدة
$text = preg_replace('/\s+/', ' ', $text);
// القص إلى الطول المحدد
if (strlen($text) > $length) {
$text = substr($text, 0, $length);
// القص عند آخر حد كلمة
$lastSpace = strrpos($text, ' ');
if ($lastSpace !== false) {
$text = substr($text, 0, $lastSpace);
}
$text .= '...';
}
return trim($text);
}
}
// app/Actions/CalculateReadingTimeAction.php
namespace App\Actions;
class CalculateReadingTimeAction
{
protected const WORDS_PER_MINUTE = 200;
public function execute(string $content): int
{
$wordCount = str_word_count(strip_tags($content));
$minutes = ceil($wordCount / self::WORDS_PER_MINUTE);
return max(1, $minutes); // حد أدنى 1 دقيقة
}
}
// app/Actions/PublishPostAction.php
namespace App\Actions;
use App\Events\PostPublished;
use App\Models\Post;
use Illuminate\Support\Facades\Cache;
class PublishPostAction
{
public function execute(Post $post): bool
{
if ($post->status === 'published') {
return false;
}
$post->update([
'status' => 'published',
'published_at' => now(),
]);
// إرسال الحدث
event(new PostPublished($post));
// مسح الذاكرة المؤقتة
Cache::forget('posts.recent');
Cache::forget('posts.published');
return true;
}
}
استخدام الإجراءات في الخدمة:
// app/Services/PostService.php (مع الإجراءات)
namespace App\Services;
use App\Actions\CalculateReadingTimeAction;
use App\Actions\GeneratePostExcerptAction;
use App\Actions\PublishPostAction;
use App\DTOs\CreatePostDTO;
use App\Models\Post;
use App\Repositories\Contracts\PostRepositoryInterface;
class PostService
{
public function __construct(
protected PostRepositoryInterface $postRepository,
protected GeneratePostExcerptAction $generateExcerpt,
protected CalculateReadingTimeAction $calculateReadingTime,
protected PublishPostAction $publishPostAction
) {}
public function createPost(CreatePostDTO $dto): Post
{
$data = $dto->toArray();
// توليد مقتطف تلقائياً إذا لم يتم توفيره
if (empty($data['excerpt'])) {
$data['excerpt'] = $this->generateExcerpt->execute($dto->content);
}
// حساب وقت القراءة
$data['reading_time'] = $this->calculateReadingTime->execute($dto->content);
// توليد slug فريد
$data['slug'] = $this->generateUniqueSlug($dto->title);
$data['author_id'] = auth()->id();
return $this->postRepository->create($data);
}
public function publishPost(int $id): bool
{
$post = $this->postRepository->find($id);
if (!$post) {
return false;
}
return $this->publishPostAction->execute($post);
}
// ... طرق أخرى
}
نمط الأمر للعمليات المعقدة
نمط الأمر يغلف الطلبات ككائنات، مما يسهل وضعها في قائمة انتظار أو تسجيلها أو التراجع عنها.
إنشاء فئات الأمر:
// app/Commands/CreatePostCommand.php
namespace App\Commands;
use App\DTOs\CreatePostDTO;
use App\Models\Post;
class CreatePostCommand
{
public function __construct(
public readonly CreatePostDTO $dto
) {}
}
// app/Commands/Handlers/CreatePostHandler.php
namespace App\Commands\Handlers;
use App\Commands\CreatePostCommand;
use App\Models\Post;
use App\Repositories\Contracts\PostRepositoryInterface;
use App\Services\PostService;
class CreatePostHandler
{
public function __construct(
protected PostService $postService
) {}
public function handle(CreatePostCommand $command): Post
{
return $this->postService->createPost($command->dto);
}
}
تنفيذ ناقل الأوامر:
// app/Services/CommandBus.php
namespace App\Services;
use Illuminate\Container\Container;
class CommandBus
{
protected $container;
protected $handlers = [];
public function __construct(Container $container)
{
$this->container = $container;
}
public function register(string $command, string $handler): void
{
$this->handlers[$command] = $handler;
}
public function dispatch($command)
{
$commandClass = get_class($command);
if (!isset($this->handlers[$commandClass])) {
throw new \Exception("No handler registered for {$commandClass}");
}
$handler = $this->container->make($this->handlers[$commandClass]);
return $handler->handle($command);
}
}
// app/Providers/CommandBusServiceProvider.php
namespace App\Providers;
use App\Commands\CreatePostCommand;
use App\Commands\Handlers\CreatePostHandler;
use App\Services\CommandBus;
use Illuminate\Support\ServiceProvider;
class CommandBusServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(CommandBus::class, function ($app) {
$bus = new CommandBus($app);
// تسجيل معالجات الأوامر
$bus->register(CreatePostCommand::class, CreatePostHandler::class);
return $bus;
});
}
}
استخدام ناقل الأوامر في Controller:
// app/Http/Controllers/PostController.php
namespace App\Http\Controllers;
use App\Commands\CreatePostCommand;
use App\DTOs\CreatePostDTO;
use App\Http\Requests\StorePostRequest;
use App\Services\CommandBus;
class PostController extends Controller
{
protected $commandBus;
public function __construct(CommandBus $commandBus)
{
$this->commandBus = $commandBus;
}
public function store(StorePostRequest $request)
{
$dto = CreatePostDTO::fromRequest($request->validated());
$command = new CreatePostCommand($dto);
$post = $this->commandBus->dispatch($command);
return redirect()
->route('posts.show', $post->slug)
->with('success', 'Post created successfully');
}
}
متى تستخدم ماذا:
- الخدمات: التنسيق بين المستودعات، معالجة منطق الأعمال
- DTOs: نقل البيانات بين الطبقات، ضمان أمان النوع
- الإجراءات: عمليات ذات مسؤولية واحدة، منطق قابل لإعادة الاستخدام
- الأوامر: عمليات معقدة تحتاج إلى قائمة انتظار أو تسجيل أو تراجع
تمرين 1: إنشاء خدمة طلب
أنشئ فئة OrderService تتعامل مع:
createOrder(CreateOrderDTO $dto)- إنشاء طلب، حساب الإجماليات، التحقق من المخزونupdateOrderStatus(int $id, string $status)- تحديث الحالة مع التحققcancelOrder(int $id)- إلغاء الطلب، استعادة المخزون، معالجة الاستردادcalculateOrderTotal(Order $order)- حساب الإجمالي بما في ذلك الضرائب والشحن
تمرين 2: تنفيذ DTOs
أنشئ DTOs لنظام إدارة المنتجات:
CreateProductDTO- name, description, price, category_id, stock, imagesUpdateProductDTO- جميع الحقول اختيارية- أضف طرق
fromRequest()وtoArray() - استخدم أمان النوع (خصائص readonly، تلميحات النوع)
تمرين 3: بناء فئات الإجراء
أنشئ فئات إجراء لنظام تجارة إلكترونية:
CalculateShippingCostAction- حساب الشحن بناءً على الوزن والوجهةApplyDiscountAction- تطبيق رمز قسيمة على الطلبProcessPaymentAction- معالجة الدفع عبر البوابةSendOrderConfirmationAction- إرسال بريد تأكيد
استخدم هذه الإجراءات في OrderService لإكمال عملية الدفع.
النقاط الرئيسية:
- طبقة الخدمة تحافظ على Controllers رفيعة ومركزة
- DTOs توفر أمان النوع وعقود بيانات واضحة
- فئات الإجراء تنفذ مبدأ المسؤولية الواحدة
- نمط الأمر يجعل العمليات قابلة للوضع في قائمة انتظار وقابلة للاختبار
- اختر النمط المناسب بناءً على التعقيد والمتطلبات
- أعطِ الأولوية دائماً لسهولة القراءة وقابلية الصيانة للكود