اذهب إلى المحتوى

Ola Abbas

الأعضاء
  • المساهمات

    189
  • تاريخ الانضمام

  • تاريخ آخر زيارة

كل منشورات العضو Ola Abbas

  1. سنتعرّف في هذه السلسلة من المقالات على محرك الألعاب جودو Godot بنسخته الرابعة والميزات التي يوفّرها، وسنبدأ مقال اليوم بشرح واجهة محرك جودو وأهم مكوناتها، ونوضّح التغييرات الرئيسية التي سنلاحظها عند الانتقال لهذا الإصدار من محرك الألعاب. ما هو محرك الألعاب جودو؟ سنتعرّف فيما يلي على محرك الألعاب جودو وسبب استخدامه والميزات التي يوفّرها. محركات الألعاب Game Engines يُعَد تطوير الألعاب أمرًا معقدًا ومثيرًا بنفس الوقت، وهو يتطلب مجموعة متنوعة من المعارف والمهارات، إذ يجب أن تتوفر لدينا الكثير من التقنيات الأساسية لبناء لعبة حديثة قبل أن تتمكّن من إنشائها، تخيل لو كان علينا بناء جهاز الحاسوب الخاص بنا وكتابة نظام التشغيل الخاص به قبل أن نبدأ حتى في البرمجة ألن يكون الأمر غاية في الصعوبة؟ سيكون تطوير الألعاب مشابهًا لذلك في حال أردنا البدء من الصفر وبناء كل شيء نحتاجه بأنفسنا. هنالك أيضًا عدد من الاحتياجات المشتركة لكل لعبة نريد بناءها، فمثلًا سنحتاج أي لعبة إلى رسم أشياء على الشاشة بغض النظر عن نوعها، لذا إذا كانت الشيفرة البرمجية اللازمة للرسم مكتوبة مسبقًا، فسيكون من المنطقي أن نعيد استخدامها بدلًا من إنشائها من جديد لكل لعبة، وهنا يأتي دور محركات الألعاب. فمحرّك الألعاب هو مجموعة من الأدوات والتقنيات المُصمَّمة لمساعدة مطوري الألعاب، مما يسمح لنا بالتركيز أكثر على بناء لعبتنا دون الحاجة لإعادة اختراع العجلة، حيث سنوضّح فيما يلي بعض الميزات التي يوفرها محرك الألعاب الجيد: التصيير أو الإخراج Rendering ثنائي الأبعاد وثلاثي الأبعاد: هو عملية عرض اللعبة على شاشة اللاعب، ويجب أن يعمل محرك التصيير الجيد مع ميزات وحدة معالجة الرسوميات GPU الحديثة، ويدعم الشاشات عالية الدقة، ويحسن المؤثرات مثل الإضاءة ومنظور اللعب Perspective مع الحفاظ في الوقت نفسه على معدّل إطارات مرتفع لضمان تجربة لعب سلسة الفيزياء Physics: يُعَد إنشاء محرك فيزيائي دقيق وقابل للاستخدام مهمة ضخمة، إذ تتطلب معظم الألعاب اكتشاف التصادم والاستجابة له، وتحتاج العديد منها إلى محاكاة فيزيائية مثل الاحتكاك والعطالة أو القصور الذاتي Inertia وغير ذلك، ولن يرغب المطورون بتكبد عناء كتابة الشيفرة البرمجية لهذه المهمة دعم المنصات: قد تحتاج لإصدار لعبتك على منصات متعددة كالهاتف المحمول والويب والحاسوب الشخصي والطرفية Console، فمحرّك الألعاب الجيد يتيح لنا بناء لعبتنا مرة واحدة وتصديرها إلى منصة أو منصات أخرى بيئة التطوير: تُجمَع كل هذه الأدوات في تطبيق واحد، مما يؤدي إلى وجود كل شيء في بيئة واحدة حتى لا تضطر إلى تعلّم سير عمل جديد لكل مشروع تعمل عليه هناك عشرات محركات الألعاب الشهيرة التي يمكننا الاختيار من بينها مثل جودو GoDot ويونتي Unity وآن ريل Unreal وغيرها. وتجدر الإشارة لأن غالبية محرّكات الألعاب الشهيرة هي منتجات تجارية، فقد تكون مجانية للتنزيل وقد لا تكون كذلك، وأنها قد تتطلّب اتفاقية ترخيص أو حقوق ملكية إذا أردنا إصدار ألعابنا وخاصة إذا حقّقت أرباحًا، لذا يجب قراءة وفهم ما نوافق عليه وما يُسمح وما لا يُسمح بفعله عندما ننوي استخدام أي محرك. لماذا نستخدم محرك ألعاب جودو يُعَد محرّك ألعاب جودو مجانيًا ومفتوح المصدر، وهو يصدر وفق ترخيص MIT المرن، فلا توجد رسوم أو تكاليف مخفية أو حقوق ملكية يجب دفعها، ويُعَد جودو محرك ألعاب حديث ومتكامل الميزات. وهو يوفر الكثير من الفوائد لمطوري الألعاب، لأنه غير مثقل بالترخيص التجاري، ويوفر لنا سيطرة كاملة على كيفية ومكان توزيع لعبتنا. كما أن طبيعة جودو مفتوحة المصدر توفر مستوى شفافية أعلى من محركات الألعاب التجارية، فمثلًا إذا وجدنا أن ميزة معينة لا تلبي احتياجاتنا تمامًا، فيمكننا تعديل المحرّك دون الحاجة إلى إذن لذلك. اكتشاف واجهة محرر الألعاب جودو سنكتشف فيما يلي واجهة محرك الألعاب جودو التي ستقضي فيها معظم وقتك عند بناء لعبتك. مدير المشروع Project Manager أول شيء سنراه عند فتح محرك ألعاب جودو هو نافذة مدير المشروع Project Manager، والتي ستبدو كما يلي: سنرى في هذه النافذة قائمة بمشاريع جودو، حيث يمكننا اختيار مشروع موجود مسبقًا والنقر على زر تشغيل Run لتشغيل اللعبة أو النقر على زر تحرير Edit للعمل على اللعبة في محرّر جودو. لنبدأ بالنقر على زر مشروع جديد New Project، في حال لم يكن لدينا أي مشاريع حتى الآن. يمكننا في النافذة السابقة إعطاء اسم للمشروع، وإنشاء مجلد لتخزينه ملفات لعبتنا فيه. ملاحظة: يوجد مشروع جودو في مجلد خاص به، ويمثل ذلك العديد من الفوائد بما في ذلك تسهيل نقل المشاريع ومشاركتها والنسخ الاحتياطي لها، ويجب أيضًا أن تكون جميع ملفات المشروع من صور وملفات صوتية وما إلى ذلك داخل مجلد المشروع. يجب أن نحرص على اختيار اسم يصف عمل مشروع لعبتنا عند تسميته، فمثلًا لا يعد الاسم New Game Project23 جيدًا لكونه لا يساعدنا في تذكر ما يفعله هذا المشروع. يجب أيضًا أن نفكر في التوافق، فقد تكون بعض أنظمة التشغيل حساسة لحالة الأحرف وبعضها الآخر غير حساس لحالة الأحرف، مما يؤدي لحدوث مشكلات إذا نقلنا المشروع أو شاركناه من حاسوب لآخر، لذا يعتمد العديد من المبرمجين قواعد تسمية موحّدة مثل عدم وجود مسافات بين الكلمات واستخدام شرطة سفلية _ بينها. سنسمّي المشروع الجديد getting_started، لذا نكتب هذا الاسم ونتأكد من تفعيل خيار أنشئ مجلد Create Folder، قبل النقر على زر إنشاء وتعديل Create & Edit أسفل النافذة. يمثل الشكل التالي نافذة محرر محرك الألعاب جودو، وهو المكان الذي سنقضي فيه معظم وقتنا عند العمل في جودو، ويكون المحرر مقسمًا إلى أقسام كما يلي: نافذة العرض Viewport: المكان الذي نرى فيه أجزاء لعبتنا أثناء العمل عليها مساحات العمل Workspaces: في الجزء العلوي الأوسط حيث يمكننا التبديل بين العمل في مساحات العمل ثنائية الأبعاد 2D أو ثلاثية الأبعاد 3D أو السكربت، ولكن تكون البداية من مساحة العمل ثلاثية الأبعاد أزرار اختبار اللعب Playtest Buttons: تتيح لنا هذه الأزرار تشغيل اللعبة والتحكم فيها أثناء الاختبار الحاويات Docks أو التبويبات Tabs: توجد على جانبي واجهة جودو عدد من الحاويات Docks والتبويبات حيث يمكننا من خلالها عرض عناصر اللعبة وضبط خاصياتها اللوحة السفلية Bottom Panel: تتضمن هذه اللوحة معلومات خاصة بالسياق لأدوات مختلفة، وأهمها لوحة الخرج Output، حيث سنرى رسائل الأخطاء أو المعلومات عند تشغيل لعبتنا إعدادات المشروع Project Settings تحدثنا عن الأجزاء الرئيسية لواجهة محرر جودو وكيفية عملها، ولنتحدّث الآن عن إعدادات المشروع، فإحدى المهام الأولى عند بدء مشروع جديد هي التأكد من صحة الإعدادات. ننقر على خيار مشروع Project من القائمة ثم نحدّد خيار إعدادات المشروع Project Settings. النافذة السابقة هي نافذة إعدادات المشروع، وتوجد على الجهة اليمنى قائمة من الفئات. تكون الإعدادات الافتراضية مناسبة لمعظم المشاريع، فلا حاجة للقلق بشأن تغييرها إلّا إن كان لدينا خيار محدد يتطلب التغيير. سنلقي نظرة حاليًا على قسم التطبيق Application ثم إعداد Config حيث يمكننا من هنا ضبط عنوان اللعبة، واختيار المشهد الرئيسي الذي سنوضّحه لاحقًا، كما يمكننا من هنا تغيير أيقونة اللعبة. القسم الثاني هو قسم الإظهار Display ثم نافذة Window، وهو المكان الذي يمكننا من تحديد طريقة ظهور لعبتنا وعرضها. إذ يمكننا ضبط العرض width والارتفاع height لضبط حجم نافذة اللعبة، فمثلًا إذا أنشأنا لعبة لهاتف محمول، فيجب ضبطها على دقة وأبعاد الجهاز المستهدف. توجد أيضًا إعدادات لتغيير الحجم Scaling والتمدّد Stretching ووضع ملء الشاشة وغير ذلك. سنترك الحجم الافتراضي كما هو حاليًا، حيث سنتحدث لاحقًا عن كيفية ضبط هذه الإعدادات بدقة لتشغيل اللعبة على أجهزة مختلفة. توجد أيضًا بعض التبويبات في الجزء العلوي من النافذة مثل التبويب عام General الذي تحدّثنا عنه. سنتحدث الآن بإيجاز عن تبويب خريطة الإدخال Input Map، وهو المكان الذي يمكننا فيه تحديد إجراءات إدخال مختلفة للتحكم في إجراءات الإدخال أي كيف نتعامل مع مدخلات لوحة المفاتيح والفأرة وغير ذلك. حيث نهتم في لعبتنا في تحديد المفتاح أو الزر الذي يضغط عليه اللاعب، وتحديد الإجراء الذي سيحدث عند الضغط عليه للتعامل مع مدخلات اللاعب بكفاءة. يوجد أيضًا تبويبات أخرى مثل تبويب التوطين Localization المخصص لدعم لغات متعددة، وتبويب إضافات Plugins التي أنشأ معظمها مجتمع محرك ألعاب جودو، والتي يمكن تنزيلها وإضافتها لتوفير مزيد من الميزات والأدوات المختلفة وغير ذلك وسنتحدث عنها لاحقًا. دليل الانتقال من الإصدار Godot 3.x للإصدار Godot 4.0 سنوضّح فيما يلي التغييرات الرئيسية والمشاكل التي يجب الانتباه إليها إذا أردتَ الانتقال إلى الإصدار 4.0 لمحرك ألعاب جودو. الأسماء الجديدة أكبر تغيير في الإصدار Godot 4 هو توفر مجموعة كاملة من عمليات إعادة التسمية للعقد والدوال وأسماء الخاصيات، حيث تجري معظم هذه العمليات لجعل الأمور متناسقة أو واضحة. إليك فيما يلي بعض التغييرات الكبرى التي يجب الانتباه إليها: العقد ثنائية الأبعاد 2D وثلاثية الأبعاد 3D: حملت العقد ثنائية الأبعاد في الإصدار Godot 3.x اللاحقة 2D، ولكن لم يكن للعقد ثلاثية الأبعاد لاحقة، لذا أصبحت الآن جميع العقد تحمل إما اللاحقة 2D أو اللاحقة 3D لجعل الأمور متناسقة مثل RigidBody2D و RigidBody3D أعيدت تسمية العقدة Spatial إلى Node3D في الفئة ثلاثية الأبعاد لتتناسب معها أعيدت تسمية واحدة من أكثر العقد شهرة وهي KinematicBody إلى CharacterBody2D أو CharacterBody3D، وسنوضّح لاحقًا مزيدًا من تغييرات الواجهة البرمجية API لهذه العقدة. أعيدت تسمية الدالة instance()‎ الخاصة بالعقدة PackedScene إلى instantiate()‎ حلت الخاصيات position و global_position محل الخاصيات translation و global_translation للعقد ثلاثية الأبعاد، مما يجعلها متوافقة مع العقد ثنائية الأبعاد الإشارات Signals والعناصر القابلة للاستدعاء Callables أصبح العمل مع الإشارات منظمًا أكثر في الإصدار 4.0، حيث أصبح النوع Signal نوعًا أصيلًا الآن، لذا سنستخدم عددًا أقل من السلاسل النصية، مما يعني أننا سنحصل على ميزة الإكمال التلقائي والتحقق من الأخطاء. ينطبق الأمر ذاته أيضًا على الدوال، والتي يمكن الآن الرجوع إليها مباشرةً بدلًا من استخدام السلاسل النصية. فيما يلي مثال لتعريف إشارة وتوصيلها وإرسالها: extends Node signal my_signal func _ready(): my_signal.connect(signal_handler) func _input(event): if event.is_action_pressed("ui_select"): my_signal.emit() func signal_handler(): print("signal received") عناصر الانتقال التدريجي Tweens في حال استخدام SceneTreeTween في الإصدار Godot 3.5، فسنكون على دراية باستخدام عناصر الانتقال التدريجي Tween التي تُستخدَم مثلًا لتغيير اللون أو الموقع الخاص بالكائن تدريجيًا في الإصدار Godot 4.0. لم يَعُد Tween عقدة، لذا يمكننا إنشاء كائنات رسوم متحركة للانتقال التدريجي Tween لمرة واحدة كلما احتجنا إليها، وستصبح أكثر قوة وأسهل في الاستخدام من الطريقة القديمة بعد التعود عليها. العقدة AnimatedSprite[2D|3D]‎ يُعَد اختفاء الخاصية playing أكبر تغيير لمستخدمي الإصدار 3‎.x‎ لهذه العقدة، حيث أصبحت أكثر تناسقًا مع استخدام AnimationPlayer، إذ يمكن تبديل التشغيل التلقائي في لوحة SpriteFrames لتشغيل الرسوم المتحركة تلقائيًا. وعلينا استخدام الدالتين play()‎ و stop()‎ في الشيفرة البرمجية للتحكم في التشغيل. العقدة CharacterBody[2D|3D]‎ أكبر تغيير في هذه العقدة هو استخدام الدالة move_and_slide()‎ التي لم تَعُد تستقبل معاملات، حيث فقد أصبحت جميع المعاملات خاصيات مُدمَجة، ويتضمن ذلك الخاصية velocity الأصيلة، لذا فلا حاجة للتصريح عن هذه الخاصيات. يمكن الاطلاع على محارف المنصة ومحارف FPS الأساسية للحصول على أمثلة تفصيلية لاستخدام هذه العقد. عقدة TileMap جُدّدت العقدة TileMap بالكامل في الإصدار 4.0 ابتداءً من كيفية إنشاء موارد TileSet إلى كيفية رسم عناصر الرقعة Tiles والتفاعل معها. مولّد الأعداد العشوائية RNG هناك بعض التغييرات على دوال توليد الأعداد العشوائية المُدمَجة مع لغة البرمجة GDScript، وهذه التغييرات هي: لم نعد بحاجة لاستدعاء الدالة randomize()‎، إذ سيكون الاستدعاء تلقائيًا. إذا أردنا الحصول على عشوائية قابلة للتكرار، نستخدم الدالة seed()‎ لضبطها على قيمة محدَّدة مسبقًا. حلّت الدالة randf_range()‎ للأعداد العشرية أو الدالة randi_range()‎ للأعداد الصحيحة محل الدالة القديمة rand_range()‎. كشف تصادم الأشعة Raycasting توجد واجهة برمجة تطبيقات جديدة عند كشف تصادم الأشعة لاكتشاف التصادم بين الكائنات في الشيفرة البرمجية، حيث تأخذ الدالة PhysicsDirectSpaceState[2D|3D].intersect_ray()‎ كائنًا خاصًا كمعامل، ويحدّد هذا الكائن خاصيات الشعاع، فمثلًا نستخدم ما يلي لكشف تصادم الأشعة في فضاء ثلاثي الأبعاد: var space = get_world_3d().direct_space_state var ray = PhysicsRayQueryParameters3D.create(position, destination) var collision = space.intersect_ray(ray) if collision: print("ray collided") الخاتمة تعرفنا في هذا المقال على محرك الألعاب جودو بنسخته الرابعة، واكتشفنا أهم ميزاته، كما استعرضنا واجهته الرئيسية مثل مدير المشروع ومحرر الألعاب وتعرفنا على كيفية استخدامه لإنشاء مشاريع جديدة وضبط الإعدادات. بالإضافة إلى ذلك، سطنا الضوء على أبرز التغييرات التي سنلاحظها عند الانتقال من الإصدار 3‎.‎x إلى الإصدار 4.0، مثل إعادة تسمية العقد والدوال وتعديل طريقة العمل مع الإشارات والعناصر القابلة للاستدعاء. سنعود لنافذة إعدادات المشروع لاحقًا، لذا لنغلقها الآن ونستعد للانتقال إلى مقالنا التالي من هذه السلسلة والذي سنشرح فيه مفهوم العقد Nodes وطريقة التعامل معها في محرك الألعاب جودو. ترجمة -وبتصرّف- للأقسام What is Godot و The Godot Editor و Migrating from 3.x من توثيقات Kidscancode. اقرأ أيضًا الرؤية التصميمية لمحرك اﻷلعاب جودو Godot كتابة برنامجك الأول باستخدام جودو Godot العقد Nodes والمشاهد Scenes في جودو Godot تعرف على أشهر محركات الألعاب Game Engines
  2. سنضيف في هذا المقال بعض الميزات المتقدمة الاختيارية لموقع مدونة جانغو Django الخاص بنا، والذي أنشأناه في المقالات السابقة من هذه السلسلة، بما في ذلك ميزة ترقيم الصفحات Pagination، والمنشورات ذات الصلة Related Posts، وميزة البحث Search. إنشاء ترقيم الصفحات Pagination في جانغو قد يكون إنشاء مرقّم صفحات فكرة جيدة عند إضافة الكثير من المنشورات إلى مدونتنا، فنحن لا نريد عرض عدد كبير جدًا من المنشورات في صفحة واحدة، لذا يجب إضافة شيفرة برمجية إضافية إلى دوال العرض view functions. لنأخذ على سبيل المثال عرض الصفحة الرئيسية home، حيث يجب أولًا أن نستورد بعض الحزم الضرورية لتفعيل الترقيم كما يلي: from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger نحدّث بعد ذلك العرض home كما يلي: def home(request): site = Site.objects.first() categories = Category.objects.all() tags = Tag.objects.all() featured_post = Post.objects.filter(is_featured=True).first() # إضافة مرقم الصفحات page = request.GET.get("page", "") # الحصول على رقم الصفحة الحالية posts = Post.objects.all().filter(is_published=True) paginator = Paginator(posts, n) # ‫عرض n منشور لكل صفحة try: posts = paginator.page(page) except PageNotAnInteger: posts = paginator.page(1) except EmptyPage: posts = paginator.page(paginator.num_pages) return render( request, "home.html", { "site": site, "posts": posts, "categories": categories, "tags": tags, "featured_post":featured_post }, ) يجب مراعاة ثلاثة شروط مختلفة في الأسطر من 12 إلى 17 في الشيفرة البرمجية السابقة، فإذا كان رقم الصفحة عددًا صحيحًا، فيجب إعادة الصفحة المطلوبة، وإن لم يكن رقم الصفحة عددًا صحيحًا، فيجب إعادة الصفحة 1، وإذا كان رقم الصفحة أكبر من عدد الصفحات، فيجب إعادة الصفحة الأخيرة. بعد أن انتهينا من إعداد الترقيم في الكود البرمجي للعرض، يتوجب علينا إضافة مرقّم الصفحات في القالب Template لكي يظهر في صفحة قائمة المنشورات، أي سنضيفه في الملف templates/vendor/list.html كما يلي: <!-- مرقم الصفحات --> <nav class="isolate inline-flex -space-x-px rounded-md mx-auto my-5 max-h-10" aria-label="Pagination" > {% if posts.has_previous %} <a href="?page={{ posts.previous_page_number }}" class="relative inline-flex items-center rounded-l-md border border-gray-300 bg-white px-2 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50 focus:z-20" > <span class="sr-only">Previous</span> <!-- Heroicon name: mini/chevron-left --> <svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" > <path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" clip-rule="evenodd" /> </svg> </a> {% endif %} {% for i in posts.paginator.page_range %} {% if posts.number == i %} <a href="?page={{ i }}" aria-current="page" class="relative z-10 inline-flex items-center border border-blue-500 bg-blue-50 px-4 py-2 text-sm font-medium text-blue-600 focus:z-20" >{{ i }}</a > {% else %} <a href="?page={{ i }}" aria-current="page" class="relative inline-flex items-center border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50 focus:z-20" >{{ i }}</a > {% endif %} {% endfor %} {% if posts.has_next %} <a href="?page={{ posts.next_page_number }}" class="relative inline-flex items-center rounded-r-md border border-gray-300 bg-white px-2 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50 focus:z-20" > <span class="sr-only">Next</span> <!-- Heroicon name: mini/chevron-right --> <svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" > <path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" /> </svg> </a> {% endif %} </nav> علينا تطبيق الشيء نفسه لجميع الصفحات التي تعرض قائمة منشورات مثل صفحة الوسوم والفئات وصفحة البحث. عرض المنشورات ذات الصلة Related Posts في جانغو نريد الآن إضافة ميزة عرض المنشورات ذات الصلة بالمنشور الحالي، أي المنشورات التي لها نفس وسوم هذا المنشور ويمكن القيام بذلك من خلال إضافة الشيفرة التالية: def post(request, slug): site = Site.objects.first() requested_post = Post.objects.get(slug=slug) categories = Category.objects.all() tags = Tag.objects.all() # المنشورات ذات الصلة ## الحصول على جميع الوسوم المتعلقة بهذا المقال post_tags = requested_post.tag.all() ## ترشيح جميع المنشورات التي تحتوي على وسوم متعلقة بالمنشور الحالي، واستبعاد المنشور الحالي related_posts_ids = ( Post.objects.all() .filter(tag__in=post_tags) .exclude(id=requested_post.id) .values_list("id") ) related_posts = Post.objects.filter(pk__in=related_posts_ids) return render( request, "post.html", { "site": site, "post": requested_post, "categories": categories, "tags": tags, "related_posts": related_posts, }, ) قد تكون الشيفرة البرمجية السابقة صعبة الفهم بعض الشيء، دعنا نحللها ونشرحها بالتفصيل، حيث يمثل السطر 3 الحصول على المنشور المطلوب باستخدام المتغير slug، ويمثل السطر 9 الحصول على جميع الوسوم Tags التي تعود إلى المنشور المطلوب. تصبح الأمور أعقد في الأسطر من 11 إلى 16، حيث يسترد التابع Post.objects.all()‎ جميع المنشورات من قاعدة البيانات، ثم يسترد التابع filter(tag__in=post_tags)‎ جميع المنشورات التي تحتوي على وسوم مرتبطة بالمنشور الحالي، ولكن لدينا مشكلتان، إذ سيُضمَّن المنشور الحالي في مجموعة الاستعلام، لذا سنستخدم التابع exclude(id=requested_post.id)‎ لاستبعاد المنشور الحالي. لنبسّط الآن المشكلة الثانية، ولنفترض أن لدينا السيناريو التالي مع وجود ثلاث منشورات وثلاثة وسوم: معرّف الوسم Tag ID اسم الوسم Tag Name 1 Tag 1 2 Tag 2 3 Tag 3 معرّف المنشور Post ID اسم المنشور Post Name 1 Post 1 2 Post 2 3 Post 3 وتكون العلاقة بين المنشورات والوسوم علاقة متعدد إلى متعدد Many-to-Many. معرّف الوسم Tag ID معرّف المنشور Post ID 1 2 1 3 1 1 2 1 2 2 2 3 3 2 معرّف المنشور Post ID معرّف الوسم Tag ID 1 1 1 2 2 1 2 2 2 3 3 1 3 2 لنفترض أن المنشور الحالي هو المنشور 2، وبالتالي ستكون الوسوم المتعلقة به هي 1 و 2 و 3، حيث سيذهب جانغو أولًا إلى الوسم 1 عند استخدام التابع filter(tag__in=post_tags)‎، ثم سيجد المنشورات المتعلقة بالوسم 1، والتي هي المنشورات 2 و 3 و 1، ثم يذهب إلى الوسم 2، ويجد المنشورات المتعلقة بالوسم 2، وينتقل أخيرًا إلى الوسم 3. يعيد التابع filter(tag__in=post_tags)‎ بعد ذلك في النهاية القائمة [2,3,1,1,2,3,2]، وستُعاد القائمة [3,1,1,3] بعد تنفيذ التابع exclude()‎، ولا نريد ذلك أيضًا، حيث نحتاج لإيجاد طريقة للتخلص من التكرارات، لذا يجب استخدام التابع values_list('id')‎ لتمرير معرّفات المنشورات إلى المتغير related_posts_ids، ثم نستخدم هذا المتغير لاسترداد المنشورات ذات الصلة، وبذلك نتخلص من التكرار. يمكننا عرض المنشورات ذات الصلة في القالب المقابل كما يلي: <!-- المنشورات ذات الصلة --> <div class="grid grid-cols-3 gap-4 my-5"> {% for post in related_posts %} <!-- المنشور --> <div class="mb-4 ring-1 ring-slate-200 rounded-md h-fit hover:shadow-md"> <a href="{% url 'post' post.slug %}" ><img class="rounded-t-md object-cover h-60 w-full" src="{{ post.featured_image.url }}" alt="..." /></a> <div class="m-4 grid gap-2"> <div class="text-sm text-gray-500"> {{ post.created_at|date:"F j, o" }} </div> <h2 class="text-lg font-bold">{{ post.title }}</h2> <p class="text-base"> {{ post.content|striptags|truncatewords:30 }} </p> <a class="bg-blue-500 hover:bg-blue-700 rounded-md p-2 text-white uppercase text-sm font-semibold font-sans w-fit focus:ring" href="{% url 'post' post.slug %}" >Read more →</a > </div> </div> {% endfor %} </div> دورة تطوير التطبيقات باستخدام لغة Python احترف تطوير التطبيقات مع أكاديمية حسوب والتحق بسوق العمل فور انتهائك من الدورة اشترك الآن تطبيق عملية البحث في جانغو سنعمل أخيرًا على إضافة ميزة البحث لمدونتنا، حيث نحتاج إلى استمارة بحث Search Form في الواجهة الأمامية، والتي ترسل استعلام بحث إلى العرض، تسترد دالة العرض السجلات الملائمة من قاعدة البيانات، وتعيد صفحة بحث تعرض النتيجة. استمارة البحث Search Form لنضيف أولًا استمارة بحث إلى الشريط الجانبي Sidebar في الملف templates/vendor/sidebar.html كما يلي: <div class="col-span-1"> <div class="border rounded-md mb-4"> <div class="bg-slate-200 p-4">Search</div> <div class="p-4"> <form action="{% url 'search' %}" method="POST" class="grid grid-cols-4 gap-2"> {% csrf_token %} <input type="text" name="q" id="search" class="border rounded-md w-full focus:ring p-2 col-span-3" placeholder="Search something..." /> <button type="submit" class="bg-blue-500 hover:bg-blue-700 rounded-md p-2 text-white uppercase font-semibold font-sans w-full focus:ring col-span-1" > Search </button> </form> </div> </div> . . . </div> لاحظ في الأسطر من 7 إلى 13 السمة name لحقل الإدخال input، حيث سنسميها q، سيُربط دخل المستخدم بالمتغير q ويُرسَل للواجهة الخلفية. إذا نقرنا على الزر في السطر 5، فسيُوجَّه المستخدم إلى عنوان URL بالاسم search، لذا يجب تسجيل نمط عنوان URL المقابل كما يلي: path('search', views.search, name='search'), عرض البحث Search View سيكون عرض البحث Search View كما يلي: def search(request): site = Site.objects.first() categories = Category.objects.all() tags = Tag.objects.all() query = request.POST.get("q", "") if query: posts = Post.objects.filter(is_published=True).filter(title__icontains=query) else: posts = [] return render( request, "search.html", { "site": site, "categories": categories, "tags": tags, "posts": posts, "query": query, }, ) قالب البحث Search Template لإنشاء قالب البحث لنكتب الكود التالي في الملف templates/search.html: {% extends 'layout.html' %} {% block title %} <title>Page Title</title> {% endblock %} {% block content %} <div class="grid grid-cols-4 gap-4 py-10"> <div class="col-span-3 grid grid-cols-1"> {% include "vendor/list.html" %} </div> {% include "vendor/sidebar.html" %} </div> {% endblock %} بعدها نحاول البحث عن شيء ما في استمارة البحث، ويجب إعادة المنشورات التي نطلبها فقط. الخلاصة بهذا نكون قد وصلنا لنهاية مقالنا الذي وضحنا فيه طريقة إضافة بعض الميزات المتقدمة الاختيارية لموقع مدونة جانغو Django الخاص بنا بما في ذلك مرقّم الصفحات Paginator والمنشورات ذات الصلة وميزة البحث، لتحسين تجربة المستخدم وتوفير طريقة أكثر تنظيمًا للوصول إلى محتوى المدونة. إذا كان لديكم أي استفسارات أو تعليقات حول هذا المقال أو حول جانغو بشكل عام، فلا تترددوا في طرحها في قسم المناقشة أسفل المقال أو في قسم الأسئلة والأجوبة في الأكاديمية. ترجمة -وبتصرّف- للمقال Django for Beginners #5 - Some Advanced Features لصاحبه Eric Hu. اقرأ أيضًا المقال السابق: إنشاء تطبيق مدونة كامل في جانغو استخدام عمليات CRUD لإدارة مدونة في جانغو مدخل إلى إطار عمل الويب جانغو Django البدء في إنشاء مدونة بسيطة في جانغو
  3. صُمِّمت مكتبة المحوّلات Transformers المتخصصة في بناء نماذج الذكاء الاصطناعي من منصة Huggingface بحيث يمكن توسيعها بسهولة، وتُكتَب النماذج Models بالكامل في مجلد فرعي محدَّد من المستودع بدون تجريد أو إخفاء لأي من تفاصيل العمل، لذا يمكننا بسهولة نسخ أي ملف نموذج وتعديله وفقًا لاحتياجاتنا. وإذا أردنا كتابة نموذج Model جديد خاص بنا، فيمكن البدء بالنموذج من الصفر. سنوضّح في هذا المقال كيفية كتابة نموذج مخصَّص وضبطه Configuration لنتمكّن من استخدامه بشكل يتوافق مع مكتبة المحوّلات Transformers، وسنوضّح كيفية مشاركته مع المجتمع مع شيفرته البرمجية ليتمكن أي شخص من استخدامه، حتى إن لم يكن موجودًا في مكتبة المحوّلات Transformers، حيث سنرى كيفية إضافة أو تعديل الوظائف التي يقدمها إطار العمل الافتراضي في مكتبة Transformers باستخدام أدوات برمجية كالخطافات Hooks والشيفرة البرمجية الخاصة بنا. سنستخدم في هذا المقال نموذج ResNet الذي هو جزء من المكتبة timm ونعدّله ليعمل كجزء من مكتبة Transformers وسنغلفه ضمن النموذج PreTrainedModel الذي يعد أساس جميع النماذج في Transformers. كتابة ضبط Configuration مخصص عند إنشاء نموذج في مكتبة Transformers، يجب علينا أولاً إعداد كائن ضبط النموذج، فضبط النموذج هو كائن يحتوي على جميع المعلومات الضرورية لبناء النموذج، ولا يمكن للنموذج أن يأخذ إلا الكائن config لتهيئته كما سنرى في القسم التالي، لذا يجب أن يكون هذا الكائن مكتملًا قدر الإمكان. ملاحظة: لا حاجة لتمرير كل وسيط بشكل فردي عند إنشاء النموذج، حيث تتبع النماذج في مكتبة المحوّلات Transformers منهجية تمرير كائن واحد config إلى التابع __init__ الخاص بالنموذج. بعد ذلك، يُمرَّر هذا الكائن بالكامل إلى الطبقات الفرعية للنموذج بدلاً من تقسيمه إلى عدة وسطاء. هذا يجعل الشيفرة البرمجية بسيطة ومنظمة من خلال الاحتفاظ بجميع الإعدادات في مكان واحد يسهل الوصول إليه، كما يساهم هذا النهج في تحسين قابلية إعادة استخدام الشيفرة البرمجية مع نماذج أخرى في مكتبة المُحوِّلات. إنشاء كائن ضبط النموذج سنأخذ في المثال التالي، بعض الإعدادات أو الوسطاء من الصنف ResNet والتي نرغب في تعديلها. بعد ذلك، ستوفر عمليات الضبط المختلفة أنواعًا متنوعة من أصناف ResNet المحتملة. ثم سنُخزّن هذه الوسطاء بعد التحقق من صحتها. from transformers import PretrainedConfig from typing import List class ResnetConfig(PretrainedConfig): model_type = "resnet" def __init__( self, block_type="bottleneck", layers: List[int] = [3, 4, 6, 3], num_classes: int = 1000, input_channels: int = 3, cardinality: int = 1, base_width: int = 64, stem_width: int = 64, stem_type: str = "", avg_down: bool = False, **kwargs, ): if block_type not in ["basic", "bottleneck"]: raise ValueError(f"`block_type` must be 'basic' or bottleneck', got {block_type}.") if stem_type not in ["", "deep", "deep-tiered"]: raise ValueError(f"`stem_type` must be '', 'deep' or 'deep-tiered', got {stem_type}.") self.block_type = block_type self.layers = layers self.num_classes = num_classes self.input_channels = input_channels self.cardinality = cardinality self.base_width = base_width self.stem_width = stem_width self.stem_type = stem_type self.avg_down = avg_down super().__init__(**kwargs) الأمور الثلاثة المهمة التي يجب تذكرها عند كتابة الضبط الخاص بنا هي كالتالي: يجب أن يرث الصنف المخصص ResnetConfig من الصنف الأب PretrainedConfig يجب أن يقبل التابع __init__ من الصنف المخصص أي وسطاء kwargs يجب تمرير هذه الوسطاء kwargs إلى الصنف الأب للتابع __init__ تعني الوراثة Inheritance التأكد من الحصول على جميع الوظائف من مكتبة المحوّلات Transformers، ويمثّل القيدان الآخران احتواء الصنف PretrainedConfig على حقول أكثر من الحقول التي نضبطها، ويجب أن يقبل ضبطنا كافة هذه الحقول ثم تُرسَل إلى الصنف الأب عند إعادة تحميل الضبط باستخدام التابع from_pretrained. لا يُعَد تحديد السمة model_type للضبط الخاص بنا بالقيمة model_type="resnet"‎ هنا إلزاميًا، إلا إذا أردنا تسجيل نموذجنا في الأصناف التلقائية Auto Classes كما سنوضح لاحقًا. يمكننا بعد ذلك إنشاء وحفظ الضبط الخاص بنا بسهولة كما نفعل مع أي ضبط نموذج آخر للمكتبة. لاحظ المثال التالي الذي يوضّح كيفية إنشاء الضبط resnet50d وحفظه: resnet50d_config = ResnetConfig(block_type="bottleneck", stem_width=32, stem_type="deep", avg_down=True) resnet50d_config.save_pretrained("custom-resnet") سيؤدي هذا لحفظ ملف بالاسم config.json ضمن المجلد custom-resnet، يمكننا بعدها إعادة تحميل ملف الضبط الخاص باستخدام التابع from_pretrained كما يلي: resnet50d_config = ResnetConfig.from_pretrained("custom-resnet") ويمكننا أيضًا استخدام أي تابع آخر من الصنف PretrainedConfig مثل التابع push_to_hub()‎ لرفع الضبط الخاص بنا إلى المستودع Hub مباشرة. كتابة نموذج مخصص أصبح لدينا ضبط مخصص لنموذجنا ResNet، ويمكننا الآن كتابة النموذج نفسه، حيث سنكتب نموذجين الأول يستخرج الميزات المخفية من مجموعة الصور مثل النموذج BertModel، والثاني لتصنيف الصور وفق الفئات المختلفة مثل النموذج BertForSequenceClassification. لن نكتب نموذج كامل بل سنكتب فقط مغلِّف wrapper بسيط للنموذج للسهولة، سيكون بمثابة هيكل بسيط للنموذج يمكننا تمرير الإعدادات أو الضبط إليه. وقبل أن نكتب الصنف ResNet أو النموذج نفسه، يجب أن نحدد أنواع الكتل في النموذج مثل basic أو bottleneck، ونحدد كيفية بناء هذه الكتل أو الطبقات في النموذج. بمجرد تحديد هذه الأمور، سنستخدم الضبط الذي حددناه سابقًا لتمرير هذه الإعدادات إلى الصنف ResNet لإنشاء النموذج بناءً على هذه الإعدادات. from transformers import PreTrainedModel from timm.models.resnet import BasicBlock, Bottleneck, ResNet from .configuration_resnet import ResnetConfig BLOCK_MAPPING = {"basic": BasicBlock, "bottleneck": Bottleneck} class ResnetModel(PreTrainedModel): config_class = ResnetConfig def __init__(self, config): super().__init__(config) block_layer = BLOCK_MAPPING[config.block_type] self.model = ResNet( block_layer, config.layers, num_classes=config.num_classes, in_chans=config.input_channels, cardinality=config.cardinality, base_width=config.base_width, stem_width=config.stem_width, stem_type=config.stem_type, avg_down=config.avg_down, ) def forward(self, tensor): return self.model.forward_features(tensor) الآن، سنعدّل التابع forward فقط بالنسبة للنموذج ResNet المخصص لتصنيف الصور، فهذا التابع يتعامل مع البيانات المدخلة، ويحدد كيف تتم معالجتها عبر طبقات النموذج للحصول على النتيجة المطلوبة، سنجري التعديل كما يلي: import torch class ResnetModelForImageClassification(PreTrainedModel): config_class = ResnetConfig def __init__(self, config): super().__init__(config) block_layer = BLOCK_MAPPING[config.block_type] self.model = ResNet( block_layer, config.layers, num_classes=config.num_classes, in_chans=config.input_channels, cardinality=config.cardinality, base_width=config.base_width, stem_width=config.stem_width, stem_type=config.stem_type, avg_down=config.avg_down, ) def forward(self, tensor, labels=None): logits = self.model(tensor) if labels is not None: loss = torch.nn.cross_entropy(logits, labels) return {"loss": loss, "logits": logits} return {"logits": logits} نلاحظ في كلتا الحالتين كيف ورثنا الصنف PreTrainedModel واستدعينا تهيئة الصنف الأب باستخدام الضبط config كما يحدث عندما نكتب وحدة torch.nn.Module عادية في PyTorch. ولا يُعَد السطر الذي يضبط config_class إلزاميًا، إلا إذا أردنا تسجيل نموذجنا في الأصناف التلقائية Auto Classes أي عندما نرغب بأن نتيح لمنصة Hugging Face تحديد النموذج تلقائيًا بناءً على الضبط كما سنوضح لاحقًا. ملاحظة: إذا كان نموذجنا مماثلًا لنموذج آخر موجود مسبقًا في المكتبة Transformers، فيمكن إعادة استخدام الضبط الخاص بهذا النموذج نفسه. يمكن جعل نموذجنا يعيد أي مخرجات نريدها، ولكن ستؤدي إعادة قاموس Dictionary كما فعلنا مع الصنف ResnetModelForImageClassification مع تضمين الخسارة عند تمرير التسميات التوضيحية Labels إلى جعل نموذجك قابلًا للاستخدام مباشرة في الصنف Trainer. يُعدّ استخدام تنسيق خرج آخر جيدًا طالما أنك تخطط لاستخدام حلقة تدريب خاصة بك أو أي مكتبة أخرى للتدريب. أصبح لدينا صنف النموذج الخاص بنا، فلننشئ الآن نموذجًا كما يلي: resnet50d = ResnetModelForImageClassification(resnet50d_config) يمكننا استخدام أي تابع من توابع الصنف PreTrainedModel مثل التابع save_pretrained()‎ أو push_to_hub()‎، حيث سنستخدم التابع الثاني في القسم التالي وسنرى كيفية دفع أوزان النموذج باستخدام الشيفرة البرمجية الخاصة بنموذجنا، ولكن لنحمّل أولًا بعض الأوزان المدرَّبة مسبقًا في نموذجنا. يمكن أن ندرّب نموذجنا المخصّص على بياناتنا الخاصة في حالة استخدامه بشكل مخصص، ولكن سنستخدم في هذا المقال النسخة المدرَّبة مسبقًا من الضبط resnet50d، وبما أن نموذجنا يحتوي على مغلِّف فقط، فسيكون من السهل نقل هذه الأوزان كما يلي: import timm pretrained_model = timm.create_model("resnet50d", pretrained=True) resnet50d.model.load_state_dict(pretrained_model.state_dict()) لنوضّح الآن كيفية التأكد من حفظ شيفرة النموذج البرمجية عند تنفيذ التابع save_pretrained()‎ أو push_to_hub()‎. تسجيل النموذج في الأصناف التلقائية Auto Classes إذا أردنا كتابة مكتبة توسّع المكتبة Transformers، فقد نرغب في توسيع الأصناف التلقائية لتضمين نموذجنا الخاص، ويختلف ذلك عن دفع الشيفرة البرمجية إلى المستودع Hub، إذ سيحتاج المستخدمون لاستيراد مكتبتنا هذه للحصول على النموذج المخصَّص على عكس تنزيل شيفرة النموذج البرمجية تلقائيًا من المستودع Hub. إذا احتوى الضبط على السمة model_type التي تختلف عن أنواع النماذج الموجودة مسبقًا واحتوت أصناف نموذجنا على سمات config_class الصحيحة، فيمكن إضافتها إلى الأصناف التلقائية كما يلي: from transformers import AutoConfig, AutoModel, AutoModelForImageClassification AutoConfig.register("resnet", ResnetConfig) AutoModel.register(ResnetConfig, ResnetModel) AutoModelForImageClassification.register(ResnetConfig, ResnetModelForImageClassification) نلاحظ أن الوسيط الأول المُستخدَم عند تسجيل ضبطنا المخصص في الصنف التلقائي AutoConfig يجب أن يتطابق مع السمة model_type لضبطنا المخصص، ويجب أن يتطابق الوسيط الأول المُستخدَم عند تسجيل النماذج المخصَّصة في أي صنف نموذج تلقائي مع السمة config_class لتلك النماذج. إرسال الشيفرة البرمجية للمستودع علينا التأكّد أولًا من تعريف نموذجنا الكامل في ملف بايثون ‎.py، حيث يمكن الاعتماد على الاستيراد النسبي لبعض الملفات الأخرى طالما أن جميع الملفات موجودة في المجلد نفسه، فالوحدات الفرعية لهذه الميزة غير مدعومة حتى الآن. سنعرّف في مثالنا ملف modeling_resnet.py وملف configuration_resnet.py في مجلد ضمن مجلد العمل الحالي resnet_model، ويحتوي ملف الضبط على الشيفرة البرمجية الخاصة بالصنف ResnetConfig، ويحتوي ملف النموذج على الشيفرة البرمجية الخاصة بالصنفين ResnetModel و ResnetModelForImageClassification. . └── resnet_model ├── __init__.py ├── configuration_resnet.py └── modeling_resnet.py يمكن أن يكون الملف ‎__init__.py فارغًا، لكنه موجود لتتمكّن لغة بايثون من اكتشاف إمكانية استخدام resnet_model كوحدة Module مما يعني أنه يمكن استيراد المكونات والملفات من هذا المجلد في برامج بايثون أخرى. ملاحظة1: إذا أردنا نسخ ملفات النموذج من المكتبة إلى مشروعنا الخاص، فيجب استبدال جميع تعليمات الاستيراد النسبية في أعلى الملف واستيرادها مباشرة من حزمة transformers. ملاحظة2: تُعدّ واجهة التطبيقات البرمجية API هذه تجريبية وقد تحتوي على بعض التغييرات في الإصدارات اللاحقة. بإمكاننا إعادة استخدام أو إنشاء صنف فرعي لضبط أو لنموذج موجود مسبقًا، ويمكن مشاركة نموذجنا مع المجتمع من خلال استيراد نموذج وضبط ResNet أولًا من الملفات التي أنشأناها كما يلي: from resnet_model.configuration_resnet import ResnetConfig from resnet_model.modeling_resnet import ResnetModel, ResnetModelForImageClassification بعد ذلك، علينا إخبار المكتبة بأننا نريد نسخ ملفات الشيفرة البرمجية لتلك الكائنات عند استخدام التابع save_pretrained وتسجيلها بطريقة صحيحة في صنف تلقائي محدّد وخاصةً بالنسبة للنماذج، لذا ننفّذ التعليمات التالية: ResnetConfig.register_for_auto_class() ResnetModel.register_for_auto_class("AutoModel") ResnetModelForImageClassification.register_for_auto_class("AutoModelForImageClassification") نلاحظ أنه لا حاجة لتحديد صنف تلقائي للضبط Config، إذ يوجد صنف تلقائي واحد فقط له هو AutoConfig، ولكن يختلف الأمر بالنسبة للنموذج Model، فالنماذج في مكتبة المحولات Transformers قد تُستَخدم في مهام مختلفة مثل توليد النصوص، أو الترجمة أو تصنيف الصور، لذا يتوجب علينا تحديد الصنف التلقائي المناسب بناءً على نوع النموذج والمهمة التي يؤديها. عندما نريد جعل نموذجنا الخاص قابلاً للاستخدام في مكتبة Transformers وتسجيله ضمن النماذج التلقائية مثل AutoModel, AutoConfig، يجب استخدام التابعregister_for_auto_class()‎ لتسجيل النموذج بشكل صحيح، وإذا كنا نفضل استخدام الشيفرة البرمجية الموجودة على المستودع Hub من مستودع آخر، فلن تحتاج لاستدعاء هذا التابع. يمكننا تعديل الملف config.json مباشرة باستخدام البنية التالية في الحالات التي يوجد فيها أكثر من صنف تلقائي: "auto_map": { "AutoConfig": "<your-repo-name>--<config-name>", "AutoModel": "<your-repo-name>--<config-name>", "AutoModelFor<Task>": "<your-repo-name>--<config-name>", }, لننشئ بعد ذلك الضبط والنماذج كما فعلنا سابقًا: resnet50d_config = ResnetConfig(block_type="bottleneck", stem_width=32, stem_type="deep", avg_down=True) resnet50d = ResnetModelForImageClassification(resnet50d_config) pretrained_model = timm.create_model("resnet50d", pretrained=True) resnet50d.model.load_state_dict(pretrained_model.state_dict()) لنتأكّد الآن من تسجيل الدخول لإرسال النموذج إلى المستودع Hub، لذا نشغّل الأمر التالي في الطرفية Terminal: huggingface-cli login أو نكتب من تطبيق المفكرة ما يلي: from huggingface_hub import notebook_login notebook_login() يمكن بعد ذلك رفع النموذج إلى فضاء الأسماء Namespace الخاص بحسابنا على Hugging Face كما يلي: resnet50d.push_to_hub("custom-resnet50d") ترفع التعليمة النموذج resnet50d إلى المستودع Hugging Face Hub باسم custom-resnet50d وتجعل النموذج متاحًا لاستخدامه مباشرة في المنصة Hugging Face. حيث تُنسَخ ملفات ‎.py للنموذج وللضبط بالإضافة إلى أوزان النموذج والضبط بتنسيق json في المجلد custom-resnet50d وستُرفَع النتيجة للمستودع Hub، ويمكننا التحقق من النتيجة في مستودع النماذج على منصة Huggingface. وللمزيد حول طريقة الدفع إلى المستودع Hub ننصح بمطالعة مقال مشاركة نموذج ذكاء اصطناعي على منصة Hugging Face . استخدام نموذج مع شيفرة برمجية مخصصة يمكن استخدام أي ضبط أو نموذج أو مرمِّز Tokenizer مع ملفات الشيفرة البرمجية المخصَّصة في مستودعها باستخدام الأصناف التلقائية والتابع from_pretrained، حيث تُفحَص جميع الملفات والشيفرات البرمجية المرفوعة إلى المستودع Hub بحثًا عن البرامج الضارة، ولمزيد من التفاصيل يُنصَح بمطالعة توثيق أمان Hub، ويجب أيضًا مراجعة شيفرة النموذج والتحقق من كاتبها لتجنّب تنفيذ شيفرة برمجية ضارة. سنضبط القيمة trust_remote_code=True لاستخدام نموذج مع شيفرة برمجية مخصصة كما يلي: from transformers import AutoModelForImageClassification model = AutoModelForImageClassification.from_pretrained("sgugger/custom-resnet50d", trust_remote_code=True) يُفضَّل أيضًا تمرير قيمة تعمية الإيداع Commit Hash إلى سمة المراجعة revision للتأكّد من أن كاتب النماذج لم يُحدّث الشيفرة البرمجية ببعض الأسطر الجديدة الضارة. commit_hash = "ed94a7c6247d8aedce4647f00f20de6875b5b292" model = AutoModelForImageClassification.from_pretrained( "sgugger/custom-resnet50d", trust_remote_code=True, revision=commit_hash ) نلاحظ وجود زر لنسخ قيمة تعمية الإيداع commit hash يمكننا من خلاله نسخ التعديل بسهولةعند تصفح سجل الإيداعات الخاص بمستودع النماذج الموجود على Hugging Face Hub. الخلاصة شرحنا في مقال اليوم كيفية كتابة نموذج مخصَّص وضبطه وطريقة استخدامه في مكتبة المحوّلات Transformers، كما شرحنا كيفية مشاركته مع المجتمع على مستودع Hugging Face Hub ليتمكّن أي شخص من استخدامه. ترجمة -وبتصرّف- للقسم Building custom models من توثيقات Hugging Face. اقرأ أيضًا المقال السابق: استخدام مكتبة المرمزات Tokenizers في منصة Hugging Face استخدام وكلاء مكتبة المحولات Transformers Agents في الذكاء الاصطناعي التوليدي تدريب المًكيَّفات PEFT Adapters بدل تدريب نماذج الذكاء الاصطناعي بالكامل استخدام التدريب الموزع ومكتبة Accelerate لتسريع تدريب نماذج الذكاء الاصطناعي
  4. توفر مكتبة Transformers من منصة Hugging Face العديد من الأدوات المفيدة لبناء وتشغيل النماذج اللغوية الحديثة. ومن بين هذه الأدوات الصنف AutoClass لتحميل النماذج مسبقة التدريب بسهولة، حيث يحمّل AutoClass الإعدادات والأوزان المدربة مسبقًا بما يتناسب مع بنية النموذج، لكن هناك بعض الحالات التي قد نحتاج فيها لتحكم أكبر في معاملات النموذج، وإنشاء نموذج مخصص دون الاعتماد على الصنف AutoClass، وهو ما سنوضحه في هذا المقال. أهمية بناء نموذج مخصص يستدل الصنف AutoClass في مكتبة المحوّلات Transformers على بنية النموذج تلقائيًا ويحمّل الضبط Configuration والأوزان المدربة مسبقًا، حيث يوصى باستخدام هذا الصنف لإنتاج شيفرة برمجية مستقلة عن نقاط التحقق Checkpoint، ولكن يمكن للمستخدمين الذين يريدون مزيدًا من التحكم في معاملات النموذج المحددة إنشاء نموذج مخصص باستخدام مكتبة المحولات Transformers من بعض الأصناف الأساسية فقط. يمكن أن يكون ذلك مفيدًا لأي شخص مهتم بدراسة أو تدريب أو تجربة نموذج من مكتبة Transformers من منصة Huggingface، لذا سنتعمق أكثر في إنشاء نموذج مخصص بدون الصنف AutoClass، حيث سنتعلم كيفية: تحميل ضبط النموذج وتخصيصه إنشاء بنية نموذج إنشاء مرمِّز Tokenizer للنص إنشاء معالج صور للمهام البصرية إنشاء مستخرج ميزات للمهام الصوتية إنشاء معالج مهام متعددة الوسائط الضبط Configuration يمثّل الضبط Configuration السمات Attributes المحدَّدة للنموذج، حيث يكون لكل ضبط خاص بالنموذج سمات مختلفة، فمثلًا تحتوي جميع نماذج معالجة اللغات الطبيعية NLP على السمات hidden_size و num_attention_heads و num_hidden_layers و vocab_size وتحدّد هذه السمات عدد رؤوس الانتباه Attention Heads أو الطبقات المخفية التي سنبني نموذجًا باستخدامها. يمكن مطالعة على سمات النموذج DistilBERT من خلال الوصول إلى صنف الضبط DistilBertConfig كما يلي: >>> from transformers import DistilBertConfig >>> config = DistilBertConfig() >>> print(config) DistilBertConfig { "activation": "gelu", "attention_dropout": 0.1, "dim": 768, "dropout": 0.1, "hidden_dim": 3072, "initializer_range": 0.02, "max_position_embeddings": 512, "model_type": "distilbert", "n_heads": 12, "n_layers": 6, "pad_token_id": 0, "qa_dropout": 0.1, "seq_classif_dropout": 0.2, "sinusoidal_pos_embds": false, "transformers_version": "4.16.2", "vocab_size": 30522 } يعرض الصنف DistilBertConfig جميع السمات الافتراضية المستخدمة لبناء النموذج DistilBertModel الأساسي، وتكون جميع السمات قابلة للتخصيص، مما يعطينا مساحة للتجريب، فمثلًا يمكننا تخصيص نموذج افتراضي بهدف: تجربة دالة تنشيط مختلفة باستخدام المعامل activation استخدام نسبة تسرب Dropout Ratio أعلى لاحتمالات الانتباه باستخدام المعامل attention_dropout >>> my_config = DistilBertConfig(activation="relu", attention_dropout=0.4) >>> print(my_config) DistilBertConfig { "activation": "relu", "attention_dropout": 0.4, "dim": 768, "dropout": 0.1, "hidden_dim": 3072, "initializer_range": 0.02, "max_position_embeddings": 512, "model_type": "distilbert", "n_heads": 12, "n_layers": 6, "pad_token_id": 0, "qa_dropout": 0.1, "seq_classif_dropout": 0.2, "sinusoidal_pos_embds": false, "transformers_version": "4.16.2", "vocab_size": 30522 } ملاحظة: معدل التسرب Dropout Ratio هو تقنية مفيدة في تدريب الشبكات العصبية تساعد على منع الإفراط في التكيّف من خلال تعطيل بعض الخلايا العصبية عشوائيًا أثناء التدريب، وهذا يزيد قدرة النموذج على التعميم ويجعله أكثر قدرة على التعامل مع بيانات جديدة. يمكننا تعديل سمات النموذج المدرَّب مسبقًا في الدالة from_pretrained()‎ كما يلي: >>> my_config = DistilBertConfig.from_pretrained("distilbert/distilbert-base-uncased", activation="relu", attention_dropout=0.4) يمكننا حفظ ضبط النموذج باستخدام الدالة save_pretrained()‎ بعد الانتهاء منه كما يلي، ويُخزَّن ملف الضبط الخاص بنا كملف JSON في مجلد الحفظ المحدَّد: >>> my_config.save_pretrained(save_directory="./your_model_save_path") يمكننا إعادة استخدام ملف الضبط من خلال تحميله باستخدام الدالة from_pretrained()‎ كما يلي: >>> my_config = DistilBertConfig.from_pretrained("./your_model_save_path/config.json") ملاحظة: يمكننا أيضًا حفظ ملف الضبط الخاص بنا على هيئة قاموس Dictionary أو حتى كمجرد فرق بين سمات الضبط المخصصة وسمات الضبط الافتراضية. يمكن الاطلاع على توثيق الضبط على منصة Huggingface لمزيد من التفاصيل. النموذج Model سننشئ الآن نموذجًا، حيث يحدّد النموذج أو كما يشار إليه أحيانًا باسم البنية Architecture ما تفعله كل طبقة وما هي العمليات التي تحدث، وتُستخدَم السمات مثل num_hidden_layers من الضبط لتحديد هذه البنية. تتشارك جميع النماذج في الصنف الأساسي PreTrainedModel وبعض التوابع المشتركة مثل تغيير حجم تضمينات الإدخال وتقليم Pruning رؤوس الانتباه الذاتي Self-attention Heads أو تقليل الأجزاء غير الضرورية أو الفائضة من النموذج لتحسين كفاءته. تكون جميع النماذج أيضًا إما الصنف الفرعي torch.nn.Module أو tf.keras.Model أو flax.linen.Module، وهذا يعني أن النماذج متوافقة مع استخدام كل إطار عمل خاص بها. في حال كنا نستخدم إطار العمل بايتورش Pytorch نحمّل سمات الضبط المخصصة الخاصة بنا في النموذج كما يلي: >>> from transformers import DistilBertModel >>> my_config = DistilBertConfig.from_pretrained("./your_model_save_path/config.json") >>> model = DistilBertModel(my_config) مما يؤدي لإنشاء نموذج مع قيم عشوائية بدل أوزان مُدرَّبة مسبقًا، ولكننا لن نتمكّن من استخدام هذا النموذج استخدامًا مفيدًا حتى ندرّبه. فالتدريب عملية مكلفة وتستغرق وقتًا طويلًا، لذا يُفضَّل استخدام نموذج مدرب مسبقًا للحصول على نتائج أفضل وأسرع مع استخدام جزء بسيط فقط من الموارد المطلوبة للتدريب، لذا سننشئ نموذجًا مدربًا مسبقًا باستخدام الدالة from_pretrained()‎ كما يلي: >>> model = DistilBertModel.from_pretrained("distilbert/distilbert-base-uncased") يُحمَّل ضبط النموذج الافتراضي تلقائيًا عند تحميل الأوزان المُدرَّبة مسبقًا إذا وفرت مكتبة المحوّلات Transformers هذا النموذج، ولكن لا يزال بإمكاننا وضع سماتنا الخاصة مكان بعض أو جميع سمات ضبط النموذج الافتراضي إذا أردنا ذلك كما يلي: >>> model = DistilBertModel.from_pretrained("distilbert/distilbert-base-uncased", config=my_config) وفي حال استخدمنا إطار العمل تنسرفلو TensorFlow، فحمّل سمات الضبط المخصصة في النموذج كما يلي: >>> from transformers import TFDistilBertModel >>> my_config = DistilBertConfig.from_pretrained("./your_model_save_path/my_config.json") >>> tf_model = TFDistilBertModel(my_config) مما يؤدي إلى إنشاء نموذج مع قيم عشوائية بدلًا من أوزان مُدرَّبة مسبقًا، ولكن لن نتمكّن من استخدام هذا النموذج استخدامًا مفيدًا حتى ندربه. إذ يُعَد التدريب عملية مكلفة وتستغرق وقتًا طويلًا، لذا يُفضّل استخدام نموذج مدرّب مسبقًا للحصول على نتائج أفضل وأسرع مع استخدام جزء بسيط فقط من الموارد المطلوبة للتدريب، لذا سننشئ نموذجًا مدربًا مسبقًا باستخدام الدالة from_pretrained()‎ كما يلي: >>> tf_model = TFDistilBertModel.from_pretrained("distilbert/distilbert-base-uncased") يُحمَّل ضبط النموذج الافتراضي تلقائيًا عند تحميل الأوزان المُدرَّبة مسبقًا إذا وفّرت مكتبة المحوّلات Transformers هذا النموذج، ولكن لا يزال بإمكاننا وضع سماتنا الخاصة مكان بعض أو جميع سمات ضبط النموذج الافتراضي إذا أردنا ذلك كما يلي: >>> tf_model = TFDistilBertModel.from_pretrained("distilbert/distilbert-base-uncased", config=my_config) رؤوس النماذج Model heads أصبح لدينا نموذج DistilBERT أساسي يعطي الحالات المخفية Hidden States التي تُمرَّر كدخل إلى رأس النموذج لإنتاج الخرج النهائي. توفر مكتبة المحوّلات Transformers رأس نموذج مختلف لكل مهمة طالما أن النموذج يدعم المهمة، أي لا يمكنك استخدام النموذج DistilBERT لمهمة التحويل من تسلسل إلى آخر Sequence-to-Sequence مثل مهمة الترجمة. في حال استخدمنا إطار العمل Pytorch مع مكتبة Transformers، فإن النموذج DistilBertForSequenceClassification مثلًا هو نموذج DistilBERT أساسي مع رأس لتصنيف التسلسل، وهو بمثابة طبقة خطية فوق الخرج المجمَّع. إذًا سننشئ هذا النموذج كما يلي: >>> from transformers import DistilBertForSequenceClassification >>> model = DistilBertForSequenceClassification.from_pretrained("distilbert/distilbert-base-uncased") يمكننا إعادة استخدام نقطة التحقق السابقة بسهولة لمهمة أخرى من خلال التبديل إلى رأس نموذج مختلف، حيث يمكنك استخدام رأس النموذج DistilBertForQuestionAnswering بالنسبة لمهمة الإجابة على سؤال كما يلي، إذ يشبه رأس الإجابة على سؤال رأس تصنيف التسلسل باستثناء أنه طبقة خطية فوق خرج الحالات المخفية: >>> from transformers import DistilBertForQuestionAnswering >>> model = DistilBertForQuestionAnswering.from_pretrained("distilbert/distilbert-base-uncased") وإذا كنا تستخدم إطار العمل تنسرفلو TensorFlow، فإن النموذج TFDistilBertForSequenceClassification مثلًا هو نموذج DistilBERT أساسي مع رأس لتصنيف التسلسل، والذي يُعَد طبقة خطية فوق الخرج المجمَّع. إذًا لننشئ هذا النموذج كما يلي: >>> from transformers import TFDistilBertForSequenceClassification >>> tf_model = TFDistilBertForSequenceClassification.from_pretrained("distilbert/distilbert-base-uncased") يمكننا إعادة استخدام نقطة التحقق السابقة بسهولة لمهمة أخرى من خلال التبديل إلى رأس نموذج مختلف، حيث يمكننا استخدام رأس النموذج TFDistilBertForQuestionAnswering بالنسبة لمهمة الإجابة على سؤال كما يلي، إذ يشبه رأس الإجابة على سؤال رأس تصنيف التسلسل باستثناء أنه طبقة خطية فوق خرج الحالات المخفية: >>> from transformers import TFDistilBertForQuestionAnswering >>> tf_model = TFDistilBertForQuestionAnswering.from_pretrained("distilbert/distilbert-base-uncased") المرمّز Tokenizer الصنف الأساسي الأخير الذي نحتاجه قبل استخدام نموذج للبيانات النصية هو المرمّز tokenizer لتحويل النص الأولي إلى موترات Tensors، حيث يوجد نوعان من المرمّزات يمكنك استخدامهما مع مكتبة المحولات Transformers هما: PreTrainedTokenizer وهو تنفيذ لغة بايثون Python للمرمّز PreTrainedTokenizerFast: هو مرمّز من مكتبة Tokenizer ويستند إلى لغة رست Rust، وتكون سرعة هذا النوع من المرمّزات ملحوظة وخاصة أثناء الترميز الدفعي Batch Tokenization بسبب تنفيذه باستخدام لغة رست. ويقدّم المرمِّز السريع توابع إضافية مثل ربط الإزاحة Offset Mapping الذي يربط الرموز Tokens بكلماتها أو محارفها الأصلية. يدعم هذان المرمِّزان التوابع الشائعة مثل التشفير وفك التشفير وإضافة رموز جديدة وإدارة الرموز الخاصة. ملاحظة: لا تدعم جميع النماذج المرمِّز السريع، لذا ألقِ نظرة على الجدول الموجود في مقال مكتبة المحوّلات Transformers من منصة Hugging Face للتحقق من دعم النموذج للمرمِّز السريع. يمكنك إنشاء مرمّز من ملف المفردات vocabulary الخاص بنا كما يلي لإنشاء خاص مرمّز بنا: >>> from transformers import DistilBertTokenizer >>> my_tokenizer = DistilBertTokenizer(vocab_file="my_vocab_file.txt", do_lower_case=False, padding_side="left") يجب أن نتذكر أن المفردات القادمة من المرمّز المخصَّص ستكون مختلفة عن المفردات التي يولّدها مرمّز النموذج المُدرَّب مسبقًا، لذا سنحتاج لاستخدام مفردات نموذج مدرب مسبقًا إذا استخدمنا نموذج مُدرَّب مسبقًا، وإلّا لن يكون للدخل أي معنى. لننشئ مرمّز باستخدام مفردات نموذج مدرب مسبقًا باستخدام الصنف DistilBertTokenizer كما يلي: >>> from transformers import DistilBertTokenizer >>> slow_tokenizer = DistilBertTokenizer.from_pretrained("distilbert/distilbert-base-uncased") ولننشئ مرمّز سريع باستخدام الصنف DistilBertTokenizerFast كما يلي: >>> from transformers import DistilBertTokenizerFast >>> fast_tokenizer = DistilBertTokenizerFast.from_pretrained("distilbert/distilbert-base-uncased") ملاحظة: سيحاول الصنف AutoTokenizer افتراضيًا تحميل مرمّز سريع، ولكن يمكنك تعطيل هذا السلوك من خلال ضبط القيمة use_fast=False في الدالة from_pretrained. معالج الصور Image Processor يعالج معالج الصور المدخلات البصرية، وهو يرث الصنف ImageProcessingMixin الأساسي، ويمكن استخدامه من خلال إنشاء معالج صور مرتبط بالنموذج الذي تستخدمه، فمثلًا يمكننا إنشاء صنف ViTImageProcessor افتراضي كما يلي، إذا كنا نستخدم النموذج ViT أو المحوّل البصري Vision Transformer لتصنيف الصور: >>> from transformers import ViTImageProcessor >>> vit_extractor = ViTImageProcessor() >>> print(vit_extractor) ViTImageProcessor { "do_normalize": true, "do_resize": true, "image_processor_type": "ViTImageProcessor", "image_mean": [ 0.5, 0.5, 0.5 ], "image_std": [ 0.5, 0.5, 0.5 ], "resample": 2, "size": 224 } ملاحظة: إن لم نكن نريد تخصيص أيّ شيء، فما علينا سوى استخدام التابع from_pretrained لتحميل معاملات معالج الصور الافتراضية للنموذج. لنعدّل الآن أحد معاملات الصنف ViTImageProcessor لإنشاء معالج الصور المخصَّص كما يلي: >>> from transformers import ViTImageProcessor >>> my_vit_extractor = ViTImageProcessor(resample="PIL.Image.BOX", do_normalize=False, image_mean=[0.3, 0.3, 0.3]) >>> print(my_vit_extractor) ViTImageProcessor { "do_normalize": false, "do_resize": true, "image_processor_type": "ViTImageProcessor", "image_mean": [ 0.3, 0.3, 0.3 ], "image_std": [ 0.5, 0.5, 0.5 ], "resample": "PIL.Image.BOX", "size": 224 } العمود الفقري Backbone تتكون نماذج الرؤية الحاسوبية من العمود الفقري Backbone والعنق Neck والرأس Head، حيث يستخرج العمود الفقري الميزات Features من صورة الدخل، ويجمع العنق الميزات المستخرجة ويحسّنها، ويُستخدم الرأس للمهمة الرئيسية مثل اكتشاف الكائنات. دعنا نبدأ بتهيئة العمود الفقري في ضبط النموذج ونحدد تحميل أوزان مدرَّبة مسبقًا أو تحميل أوزان مُهيَّأة عشوائيًا، ثم يمكننا تمرير ضبط النموذج إلى الرأس. إذا أردنا مثلًا تحميل العمود الفقري ResNet في النموذج MaskFormer باستخدام رأس تقسيم أجزاء الصورة كما يلي: <hfoptions id="backbone"> <hfoption id="pretrained weights"> فيجب ضبط القيمة use_pretrained_backbone=True لتحميل أوزان ResNet المدرَّبة مسبقًا للعمود الفقري كما يلي: from transformers import MaskFormerConfig, MaskFormerForInstanceSegmentation config = MaskFormerConfig(backbone="microsoft/resnet-50", use_pretrained_backbone=True) # ضبط العمود الفقري والعنق model = MaskFormerForInstanceSegmentation(config) # الرأس وإذا أردنا تحميل العمود الفقري ResNet في النموذج MaskFormer باستخدام رأس تقسيم أجزاء الصورة كما يلي: </hfoption> <hfoption id="random weights"> فيجب ضبط القيمة use_pretrained_backbone=False لتهيئة العمود الفقري ResNet عشوائيًا كما يلي: from transformers import MaskFormerConfig, MaskFormerForInstanceSegmentation config = MaskFormerConfig(backbone="microsoft/resnet-50", use_pretrained_backbone=False) # ضبط العمود الفقري والعنق model = MaskFormerForInstanceSegmentation(config) # الرأس يمكن أيضًا تحميل ضبط العمود الفقري بطريقة منفصلة ثم تمريره إلى ضبط النموذج كما يلي: from transformers import MaskFormerConfig, MaskFormerForInstanceSegmentation, ResNetConfig backbone_config = ResNetConfig() config = MaskFormerConfig(backbone_config=backbone_config) model = MaskFormerForInstanceSegmentation(config) تُحمَّل نماذج المكتبة timm ضمن نموذج كما يلي باستخدام القيمة use_timm_backbone=True أو باستخدام الصنف TimmBackbone والصنف TimmBackboneConfig: </hfoption> </hfoptions id="timm backbone"> لذا سنستخدم القيمة use_timm_backbone=True و use_pretrained_backbone=True لتحميل أوزان timm المدرَّبة مسبقًا للعمود الفقري كما يلي: from transformers import MaskFormerConfig, MaskFormerForInstanceSegmentation config = MaskFormerConfig(backbone="resnet50", use_pretrained_backbone=True, use_timm_backbone=True) # ضبط العمود الفقري والعنق model = MaskFormerForInstanceSegmentation(config) # الرأس ولنضبط الآن القيمة use_timm_backbone=True و use_pretrained_backbone=False لتحميل العمود الفقري timm المهيَّأ عشوائيًا كما يلي: from transformers import MaskFormerConfig, MaskFormerForInstanceSegmentation config = MaskFormerConfig(backbone="resnet50", use_pretrained_backbone=False, use_timm_backbone=True) # ضبط العمود الفقري والعنق model = MaskFormerForInstanceSegmentation(config) # الرأس يمكننا أيضًا تحميل ضبط العمود الفقري واستخدامه لإنشاء الصنف TimmBackbone أو تمريره إلى ضبط النموذج، حيث ستحمّل الأعمدة الفقرية Timm الأوزان المدرَّبة مسبقًا افتراضيًا، لذا سنضبط القيمة use_pretrained_backbone=False لتحميل الأوزان المُهيَّأة عشوائيًا كما يلي: from transformers import TimmBackboneConfig, TimmBackbone backbone_config = TimmBackboneConfig("resnet50", use_pretrained_backbone=False) # إنشاء صنف العمود الفقري backbone = TimmBackbone(config=backbone_config) # إنشاء نموذج باستخدام العمود الفقري‫ timm from transformers import MaskFormerConfig, MaskFormerForInstanceSegmentation config = MaskFormerConfig(backbone_config=backbone_config) model = MaskFormerForInstanceSegmentation(config) مستخرج الميزات Feature Extractor يعالج مستخرج الميزات المدخلات الصوتية، وهو يرث الصنف FeatureExtractionMixin الأساسي، ويمكن أن يرث أيضًا الصنف SequenceFeatureExtractor لمعالجة المدخلات الصوتية. لننشئ الآن مستخرج ميزات مرتبط بالنموذج الذي تستخدمه مثل إنشاء صنف Wav2Vec2FeatureExtractor افتراضي كما يلي إذا كنت تستخدم النموذج Wav2Vec2 لتصنيف الأصوات: >>> from transformers import Wav2Vec2FeatureExtractor >>> w2v2_extractor = Wav2Vec2FeatureExtractor() >>> print(w2v2_extractor) Wav2Vec2FeatureExtractor { "do_normalize": true, "feature_extractor_type": "Wav2Vec2FeatureExtractor", "feature_size": 1, "padding_side": "right", "padding_value": 0.0, "return_attention_mask": false, "sampling_rate": 16000 } ملاحظة: إن لم نكن نرغب بتخصيص أيّ شيء، فما علينا سوى استخدام التابع from_pretrained لتحميل معاملات مستخرج الميزات الافتراضية الخاصة بالنموذج. لنعدّل الآن أحد معاملات الصنف Wav2Vec2FeatureExtractor لإنشاء مستخرج الميزات المخصَّص الخاص بنا كما يلي: >>> from transformers import Wav2Vec2FeatureExtractor >>> w2v2_extractor = Wav2Vec2FeatureExtractor(sampling_rate=8000, do_normalize=False) >>> print(w2v2_extractor) Wav2Vec2FeatureExtractor { "do_normalize": false, "feature_extractor_type": "Wav2Vec2FeatureExtractor", "feature_size": 1, "padding_side": "right", "padding_value": 0.0, "return_attention_mask": false, "sampling_rate": 8000 } المعالج Processor تقدم مكتبة المحوِّلات Transformers صنف المعالج الذي يغلِّف أصناف المعالجة مثل مستخرج الميزات والمرمِّز في كائن واحد بالنسبة للنماذج التي تدعم المهام متعددة الوسائط. لنستخدم مثلًا الصنف Wav2Vec2Processor لمهمة التعرّف التلقائي على الكلام Automatic Speech Recognition أو ASR اختصارًا، والتي تحوّل الصوت إلى نص، لذا ستحتاج إلى مستخرج ميزات ومرمّز. لننشئ أولًا مستخرج ميزات للتعامل مع المدخلات الصوتية كما يلي: >>> from transformers import Wav2Vec2FeatureExtractor >>> feature_extractor = Wav2Vec2FeatureExtractor(padding_value=1.0, do_normalize=True) ثم ننشئ مرمّز للتعامل مع المدخلات النصية كما يلي: >>> from transformers import Wav2Vec2CTCTokenizer >>> tokenizer = Wav2Vec2CTCTokenizer(vocab_file="my_vocab_file.txt") ثم ندمج مستخرج الميزات والمرمّز في الصنف Wav2Vec2Processor كما يلي: >>> from transformers import Wav2Vec2Processor >>> processor = Wav2Vec2Processor(feature_extractor=feature_extractor, tokenizer=tokenizer) الخلاصة يمكننا إنشاء أي من النماذج التي تدعمها مكتبة المحولات Transformers من منصة Huggingface باستخدام صنفين أساسيين للضبط والنموذج وصنف إضافي للمعالجة المسبَقة مثل مرمِّز أو معالج صور أو مُستخرج ميزات أو معالج، وتكون هذه الأصناف الأساسية قابلة للضبط، مما يسمح لنا باستخدام السمات المحدَّدة التي نريدها، ويمكن بسهولة إعداد نموذج للتدريب أو تعديل نموذج مُدرَّب مسبقًا لصقله Fine-tune. ترجمة -وبتصرّف- للقسم Create a custom architecture من توثيقات Hugging Face. اقرأ أيضًا تثبيت مكتبة المحوّلات Transformers ما هي منصة Hugging Face للذكاء الاصطناعي مشاركة نموذج ذكاء اصطناعي على منصة Hugging Face بناء روبوتات للعب الألعاب باستخدام طريقة التعلم المعزز ومشتقاتها باستخدام مكتبة TensorFlow بناء شبكة عصبية للتعرف على الأرقام المكتوبة بخط اليد باستخدام مكتبة TensorFlow
  5. نشرح في هذا المقال كيفية استخدام مكتبة ترميز النصوص Tokenizers التي توفرها منصة Hugging Face ونشرح طريقة استخدامها لتقسيم النصوص إلى رموز أو وحدات صغيرة تسمى Tokens، كما نوضح الخطوات المتبعة لإنشاء مقسِّم نصوص باستخدام خوارزمية ترميز زوج البتات Byte Pair Encoding التي توفرها المكتبة ونشرح طريقة استخدامه وتدريبه على بيانات مخصصة. إنشاء مرمز نصوص سنستخدم الصنف PreTrainedTokenizerFast من المكتبة Tokenizers التابعة لمنصة Hugging Face والتي توفر لنا العديد من التوابع لترميز النصوص بسرعة وكفاءة، كما تتيح لنا إمكانية تحميل المُرمِّزات التي أنشأناها بسهولة للعمل داخل مكتبة المحولات Transformers، مما يسهل دمجها مع النماذج اللغوية. لنفهم أساسيات بناء مرمِّز مخصص باستخدام مكتبة Tokenizers من أجل تخصيصه لبيانات محددة أو تطبيقات خاصة، بدلاً من الاعتماد على مرمزات جاهزة قد لا تكون مثالية لجميع الحالات. لنبدأ أولًا بإنشاء مرمِّز تجريبي كما يلي قبل الدخول بالتفاصيل: >>> from tokenizers import Tokenizer >>> from tokenizers.models import BPE >>> from tokenizers.trainers import BpeTrainer >>> from tokenizers.pre_tokenizers import Whitespace >>> tokenizer = Tokenizer(BPE(unk_token="[UNK]")) >>> trainer = BpeTrainer(special_tokens=["[UNK]", "[CLS]", "[SEP]", "[PAD]", "[MASK]"]) >>> tokenizer.pre_tokenizer = Whitespace() >>> files = [...] >>> tokenizer.train(files, trainer) أصبح لدينا الآن مُرمِّز مُدرَّب على الملفات التي حددناها والتي تحتوي على النصوص المستخدمة لتدريب المرمِّز، وبالتالي يمكننا الاستمرار في استخدامه في وقت التشغيل أو حفظه في ملف JSON لإعادة استخدامه لاحقًا. التحميل المباشر من كائن المرمز لنوضح الآن كيف يمكننا الاستفادة من كائن المرمِّز الذي أنشأناه في الفقرة السابقة داخل مكتبة المحوّلات Transformers المخصصة لمعالجة اللغات الطبيعية NLP، حيث يسمح الصنف PreTrainedTokenizerFast بإنشاء نسخ جديدة بسهولة من خلال قبول نسخة كائن المرمِّز tokenizer كوسيط كما يلي: >>> from transformers import PreTrainedTokenizerFast >>> fast_tokenizer = PreTrainedTokenizerFast(tokenizer_object=tokenizer) ويمكن الآن استخدام هذا الكائن مع جميع التوابع المشتركة بين مرمِّزات مكتبة المحوّلات Transformers والتي تساعد في تحويل النصوص إلى تمثيلات قابلة للاستخدام في النماذج اللغوية، مما يسهل عملية التدريب والتنبؤ باستخدام نماذج المحوّلات المختلفة. تحميل المرمز من ملف JSON يمكن تحميل مرمَّز من ملف JSON من خلال حفظ المرمِّز أولًا كما يلي: >>> tokenizer.save("tokenizer.json") يمكننا بعد ذلك تمرير المسار الذي حفظنا فيه هذا الملف إلى تابع التهيئة الخاص بهذا الصنف PreTrainedTokenizerFast باستخدام المعامل tokenizer_file كما يلي: >>> from transformers import PreTrainedTokenizerFast >>> fast_tokenizer = PreTrainedTokenizerFast(tokenizer_file="tokenizer.json") ويمكن الآن استخدام هذا الكائن مع جميع التوابع المشتركة بين مرمِّزات مكتبة المحوِّلات Transformers. استخدام النماذج متعددة اللغات Multilingual Models للاستدلال توفر مكتبة المحوِّلات Transformers العديد من النماذج متعددة اللغات، وهي نماذج مدربة على بيانات متعددة اللغات ويمكنها التعامل مع نصوص بلغات مختلفة، يختلف استخدام هذه النماذج في الاستدلال والتنبؤ وتحليل النصوص عن النماذج التي تدعم لغة واحدة فقط في بعض الأحيان، ومع ذلك، يمكن استخدام معظم النماذج متعددة اللغات بنفس طريقة استخدام النماذج أحادية اللغة. على سبيل المثال، يمكننا استخدام نموذج مثل bert/bert-base-multilingual-uncased من جوجل بنفس طريقة استخدام النماذج أحادية اللغة، لكننا سنركز في الفقرات التالية على شرح النماذج متعددة اللغات التي يختلف استخدامها عن استخدام النماذج أحادية اللغة لإجراء عمليات الاستدلال. نموذج XLM يحتوي نموذج XLM متعدد اللغات على عشر نقاط تحقق Checkpoints مختلفة تمثل حالات مختلفة من تدريب النموذج، وتكون نقطة واحدة منها فقط أحادية اللغة بينما تتعامل النقاط التسع الأخرى مع لغات متعددة، ويمكن تقسيم هذه النقاط التسع إلى فئتين هما: نقاط التحقق التي تستخدم تضمينات اللغة Language Embeddings للتمييز بين اللغات المختلفة ونقاط التحقق التي لا تستخدم هذه التضمينات. ملاحظة: التضمينات اللغوية Language Embeddings هي طريقة لتحويل الكلمات والجمل إلى أرقام يمكن للنماذج الحاسوبية التعامل معها، الهدف منها هو جعل الحاسوب قادرًا على فهم اللغة البشرية ومعاني الكلمات بناءً على سياقها، فكلما كانت الكلمات ذات معاني مشابهة، ستكون تضميناتها العددية أقرب لبعضها وتستخدم في مجال الترجمة الآلية وتصنيف النصوص وتحليل المشاعر. نماذج XLM التي تستخدم تضمينات اللغة تستخدم نماذج XLM التالية تضمينات اللغة لتحديد اللغة المستخدمة في الاستدلال: FacebookAI/xlm-mlm-ende-1024 لنمذجة اللغة المقنّعة Masked Language Modeling، ويدعم اللغتين الإنجليزية والألمانية FacebookAI/xlm-mlm-enfr-1024 لنمذجة اللغة المقنّعة، ويدعم الإنجليزية والفرنسية FacebookAI/xlm-mlm-enro-1024 لنمذجة اللغة المقنّعة، ويدعم الإنجليزية والرومانية FacebookAI/xlm-mlm-xnli15-1024 لنمذجة اللغة المقنعة، ويعمل مع مجموعة اللغات المدرجة في مجموعة بيانات XNLI FacebookAI/xlm-mlm-tlm-xnli15-1024 لنمذجة اللغة المقنعة والترجمة، ويعمل مع لغات XNLI FacebookAI/xlm-clm-enfr-1024 لنمذجة اللغة السببية Causal Language Modeling ويعمل مع اللغة الإنجليزية والفرنسية FacebookAI/xlm-clm-ende-1024 لنمذجة اللغة السببية ويعمل مع اللغتين الإنجليزية والألمانية يُمثَّل تضمين اللغة على شكل موتر Tensor وهو بنية على شكل مصفوفة متعددة الأبعاد لها نفس حجم بنية input_ids المُمرَّرة إلى النموذج، وتعتمد القيم الموجودة في هذه الموترات على اللغة المستخدمة وتحددها السمات التالية lang2id و id2lang الخاصة بالمرمّز. ملاحظة: نمذجة اللغة السببية Causal Language Modeling هي نوع من نماذج تعلم الآلة تهدف إلى فهم وتوليد النصوص بناء على الترتيب السببي للكلمات، في هذا النوع من النمذجة يتنبأ النموذج بالكلمة التالية في تسلسل الكلمات بناءً على الكلمات التي جاءت قبلها فقط وليس بعدها. أي يتعامل النموذج مع النص بشكل أحادي الاتجاه من اليسار لليمين ويتوقع الكلمة التالية بناءً على الكلمات السابقة لها. على سبيل المثال لترميز الجملة "Wikipedia was used to" باستخدام المرمِّز Tokenizer وتحويلها لتسلسل من الأرقام التعريفية IDs التي يمكن للنموذج معالجتها سنحمّل بداية نقطة تحقق نموذج FacebookAI/xlm-clm-enfr-1024: >>> import torch >>> from transformers import XLMTokenizer, XLMWithLMHeadModel >>> tokenizer = XLMTokenizer.from_pretrained("FacebookAI/xlm-clm-enfr-1024") >>> model = XLMWithLMHeadModel.from_pretrained("FacebookAI/xlm-clm-enfr-1024") تحتوي السمة lang2id الخاصة بالمرمِّز على اللغات المدعومة في النموذج ومعرّفاتها IDs، كما في المثال التالي: >>> print(tokenizer.lang2id) {'en': 0, 'fr': 1} لننشئ بعد ذلك الدخل التالي: >>> input_ids = torch.tensor([tokenizer.encode("Wikipedia was used to")]) # حجم الدفعة هو 1 ثم نضبط معرّف اللغة على القيمة "en" ونستخدمه لتحديد تضمين اللغة وهو موتر tensor يحتوي على القيمة 0 التي تمثل معرّف اللغة الإنجليزية، ويجب أن يكون له نفس حجم البنية input_ids. >>> language_id = tokenizer.lang2id["en"] # 0 >>> langs = torch.tensor([language_id] * input_ids.shape[1]) # torch.tensor([0, 0, 0, ..., 0]) >>> # ‫نعيد تشكيله ليكون بحجم (batch_size, sequence_length) >>> langs = langs.view(1, -1) # ‫أصبح الآن بالشكل ‎[1, sequence_length]‎ (حجم الدفعة هو 1) ويمكنك الآن تمرير البنية input_ids وتضمين اللغة إلى النموذج من أجل فهم النص وتحليله كما يلي: >>> outputs = model(input_ids, langs=langs) سيؤدي تنفيذ السكربت run_generation.py إلى توليد نص مع تضمينات اللغة باستخدام نقاط تحقق xlm-clm. نماذج XLM التي لا تستخدم تضمينات اللغة لا تتطلب نماذج XLM التالية تضمينات اللغة أثناء الاستدلال إذ يستطيع النموذج فهم اللغة التي يتعامل معها بدون الحاجة إلى معرّف اللغة وهي: FacebookAI/xlm-mlm-17-1280 لنمذجة اللغة المقنّعة ويدعم 17 لغة FacebookAI/xlm-mlm-100-1280 لنمذجة اللغة المقنّعة ويدعم 100 لغة تستخدم هذه النماذج تمثيلات الجمل المُعمَّمة Generic Sentence Representations، على عكس نقاط تحقق نموذج XLM السابقة. نموذج BERT يمكن استخدام نماذج BERT التالية للمهام متعددة اللغات: google-bert/bert-base-multilingual-uncased لنمذجة اللغة المقنعة وتوقع الجملة التالية، تدعم 102 لغة google-bert/bert-base-multilingual-cased لنمذجة اللغة المقنعة وتوقع الجملة التالية، وتدعم 104 لغات لا تتطلب هذه النماذج تضمينات اللغة أثناء الاستدلال، فهي تحدِّد اللغة من السياق وتستدل عليها وفقًا لذلك. نموذج XLM-RoBERTa يمكن استخدام نماذج XLM-RoBERTa التالية للمهام متعددة اللغات: FacebookAI/xlm-roberta-base لنمذجة اللغة المقنعة، وتدعم 100 لغة FacebookAI/xlm-roberta-large لنمذجة اللغة المقنعة، وتدعم 100 لغة دُرِّب نموذج XLM-RoBERTa على 2.5 تيرابايت من بيانات CommonCrawl المُنشَأة والمُنظَّفة حديثًا وذلك في 100 لغة، ويحقق هذا النموذج تحسينات كبيرة بالمقارنة مع النماذج متعددة اللغات الصادرة سابقًا مثل mBERT أو XLM في المهام النهائية مثل التصنيف Classification والوسم أو تحديد التسميات تسلسليًا Sequence Labeling والإجابة على الأسئلة Question Answering. نموذج M2M100 يمكن استخدام نماذج M2M100 التالية للترجمة متعددة اللغات: facebook/m2m100_418M للترجمة facebook/m2m100_1.2B للترجمة لنحمّل مثلًا نقطة تحقق النموذج facebook/m2m100_418M للترجمة من الصينية إلى الإنجليزية، حيث يمكنك ضبط لغة المصدر في المرمِّز كالتالي: >>> from transformers import M2M100ForConditionalGeneration, M2M100Tokenizer >>> en_text = "Do not meddle in the affairs of wizards, for they are subtle and quick to anger." >>> chinese_text = "不要插手巫師的事務, 因為他們是微妙的, 很快就會發怒." >>> tokenizer = M2M100Tokenizer.from_pretrained("facebook/m2m100_418M", src_lang="zh") >>> model = M2M100ForConditionalGeneration.from_pretrained("facebook/m2m100_418M") ونرمِّز النص كما يلي: >>> encoded_zh = tokenizer(chinese_text, return_tensors="pt") يفرض النموذج M2M100 أن يكون معرّف اللغة المستهدفة هو أول Token مُولَّد لترجمته إلى اللغة المستهدفة، لذا نضبط المعرّف forced_bos_token_id على القيمة en في التابع generate للترجمة إلى اللغة الإنجليزية كما يلي: >>> generated_tokens = model.generate(**encoded_zh, forced_bos_token_id=tokenizer.get_lang_id("en")) >>> tokenizer.batch_decode(generated_tokens, skip_special_tokens=True) 'Do not interfere with the matters of the witches, because they are delicate and will soon be angry.' النموذج MBart يمكن استخدام نماذج MBart التالية للترجمة متعددة اللغات: facebook/mbart-large-50-one-to-many-mmt للترجمة الآلية متعددة اللغات من لغة إلى عدة لغات، ويدعم 50 لغة facebook/mbart-large-50-many-to-many-mmt للترجمة الآلية متعددة اللغات من عدة لغات إلى عدة لغات، ويدعم 50 لغة facebook/mbart-large-50-many-to-one-mmt للترجمة الآلية متعددة اللغات من عدة لغات إلى لغة واحدة، ويدعم 50 لغة facebook/mbart-large-50 للترجمة متعددة اللغات، ويدعم 50 لغة facebook/mbart-large-cc25 للترجمة الآلية متعددة اللغات، ويعمل مع 25 لغة لنحمّل مثلًا نقطة تحقق النموذج facebook/mbart-large-50-many-to-many-mmt لترجمة اللغة الفنلندية إلى اللغة الإنجليزية، ويمكنك ضبط لغة المصدر في المرمِّز كما يلي: >>> from transformers import AutoTokenizer, AutoModelForSeq2SeqLM >>> en_text = "Do not meddle in the affairs of wizards, for they are subtle and quick to anger." >>> fi_text = "Älä sekaannu velhojen asioihin, sillä ne ovat hienovaraisia ja nopeasti vihaisia." >>> tokenizer = AutoTokenizer.from_pretrained("facebook/mbart-large-50-many-to-many-mmt", src_lang="fi_FI") >>> model = AutoModelForSeq2SeqLM.from_pretrained("facebook/mbart-large-50-many-to-many-mmt") ونرمِّز النص كما يلي: >>> encoded_en = tokenizer(en_text, return_tensors="pt") يفرض النموذج MBart معرّف اللغة المستهدفة بوصفه أول رمز مُولَّد لترجمته إلى اللغة المستهدفة، لذا اضبط المعرّف forced_bos_token_id على القيمة en في التابع generate للترجمة إلى اللغة الإنجليزية كما يلي: >>> generated_tokens = model.generate(**encoded_en, forced_bos_token_id=tokenizer.lang_code_to_id["en_XX"]) >>> tokenizer.batch_decode(generated_tokens, skip_special_tokens=True) "Don't interfere with the wizard's affairs, because they are subtle, will soon get angry." إذا أدرتَ استخدام نقطة تحقق النموذج facebook/mbart-large-50-many-to-one-mmt، فلن تحتاج إلى فرض معرّف اللغة المستهدفة بوصفه أول رمز مُولَّد، وإلّا فسيبقى الاستخدام نفسه مع النماذج الأخرى. الخلاصة تعلمنا في هذا المقال كيفية استخدام مكتبة المرمزات Tokenizers من منصة Hugging Face، والتي تتيح تقسيم النصوص إلى رموز أو وحدات صغيرة Token. كما تناولنا طريقة إنشاء مرمِّز باستخدام أسلوب BPE وتدريبه على بيانات مخصصة. وشرحنا كيفية استخدام المرمِّز المُدرَّب داخل مكتبة المحولات Transformers وكيفية حفظه لإعادة استخدامه. واستعرضنا النماذج مثل XLM وBERT و XLM-RoBERTa التي تدعم تحليل النصوص بلغات متعددة وتستخدم لتطبيقات متنوعة مثل الترجمة والتنبؤ وتحليل النصوص. اقرأ أيضًا جولة سريعة للبدء مع مكتبة المحوّلات Transformers تعرف على مكتبة المحوّلات Transformers من منصة Hugging Face مشاركة نموذج ذكاء اصطناعي على منصة Hugging Face تثبيت مكتبة المحوّلات Transformers
  6. لقد تعرفنا في المقالات السابقة على على كيفية ضرب المصفوفات بمصفوفة عمودية وكذلك كيفية حساب عمليات ضرب المصفوفات لعمل تصاميم ثلاثية الأبعاد 3D وغيرها، وسنتابع في هذا المقال، شرح آخر نوع من عمليات ضرب المصفوفات، إذ يوضّح هذا المقال خاصيات أخرى لعملية ضرب المصفوفات، خاصةً المصفوفة المحايدة Identity Matrix ومعكوس المصفوفة Matrix Inverse. سنوضّح في هذا المقال المواضيع التالية: المصفوفة المحايدة: ‎IA = AI = A‎ المصفوفة المحايدة هي مصفوفة فريدة منقول Transpose المصفوفة المحايدة: ‎IT = I‎ ضرب المصفوفة المحايدة بنفسها: ‎II = I‎ المصفوفة المحايدة الموجودة في المنتصف بين مصفوفتين: ‎AIB = AB‎ معكوس مصفوفة: ‎AA-1 = A-1A = I‎ المصفوفات المفردة Singular وغير المفردة Non-singular معكوس المصفوفة هو مصفوفة فريدة معكوس ضرب مصفوفتين: ‎(AB)-1 = B-1 A-1‎‎ معكوس منقول المصفوفة: ‎‎(A-1)T = (AT)-1‎‎ محدِّد Determinant المصفوفة المفردة ستكون جميع المصفوفات في هذا المقال مصفوفات مربعة؛ فالمصفوفة المربعة هي المصفوفة التي يكون فيها عدد الصفوف مساويًا لعدد الأعمدة. الضرب الملحق Post-multiplication للمصفوفات في المصفوفة المحايدة تستخدم الرسوميات الحاسوبية المصفوفات المربعة لأغراض متعددة؛ إذ تُطبَّق معظم عمليات التحويل Transformations، مثل تغيير مجال الرؤية في العالم ثلاثي الأبعاد أو عرضها على صورة ثنائية الأبعاد، باستخدام المصفوفات المربعة. وكما هو معروف، للمصفوفات المربعة خاصيات لا تتشارك فيها مع المصفوفات الأخرى، لذا سنتعرف أولًا على إحدى هذه الخاصيات من خلال إجراء عملية الضرب التالية: وبالتالي نلاحظ أن الضرب الملحق لمصفوفةٍ ما بالمصفوفة المحايدة يعطي المصفوفة نفسها: ‎AI = A‎‎ الضرب المسبق Pre-multiplication للمصفوفات بالمصفوفة المحايدة لنجرِ الآن عملية الضرب التالية: سنلاحظ أن الضرب المسبق لمصفوفةٍ ما بالمصفوفة المحايدة يعطي المصفوفة نفسها: ‎IA = A‎‎ المصفوفة المحايدة Identity Matrix تُسمَّى المصفوفة I بالمصفوفة المحايدة، لأن ضرب المصفوفة بها، يعطي المصفوفة نفسها: IA = A و ‎AI = A‎، وذلك مهما كانت المصفوفة A؛ وهي مشابهة للعدد الحقيقي 1 الذي يمثل العنصر المحايد لعملية ضرب الأعداد الحقيقية، لأن 1a = a و a1 = a لجميع الأعداد الحقيقية a. لا توجد مصفوفة محايدة مُحدَّدة للمصفوفات بجميع أبعادها؛ إذ تكون المصفوفة ‎IN×N‎ مصفوفةًَ محايدةً للمصفوفات المربعة التي أبعادها N×N فقط، فمثلًا المصفوفة المحايدة للمصفوفات التي أبعادها 3×3 هي: نضع العدد 1 مكان عناصر القطر الرئيسي Main Diagonal للمصفوفة المحايدة ذات N بُعد ‎IN×N‎، والعدد 0 للعناصر الأخرى في المصفوفة؛ إذ يتكون القطر الرئيسي من العناصر التي يتساوى فيها رقم الصف مع رقم العمود، ويجب أن تكون الأرقام من النوع 1 على هذا القطر، وليس على القطر الآخر. المصفوفة المحايدة هي مصفوفة فريدة لا توجد مصفوفة أخرى أبعادها N×N تماثل في عملها المصفوفة المحايدة؛ فبما أن العدد 1 فريدٌ بالنسبة للأعداد الحقيقية، فإن المصفوفة المحايدة هي مصفوفةٌ فريدة بالنسبة للمصفوفات التي أبعادها NxN، وسنبرهن ذلك فيما يلي: لنفترض أن جميع المصفوفات لها الأبعاد الصحيحة لعملية الضرب، وأن هناك مصفوفة Z تحقق ما يلي: ‎ZA = A‎ (1)‎ مهما كانت المصفوفة A التي لها الأبعاد الصحيحة، فستعمل المصفوفة Z مثل عمل المصفوفة I، ولكن نأمل أن تكون مختلفةً عنها؛ أما بالنسبة للمصفوفة B فنعلم أن ضرب مصفوفة B بالمصفوفة المحايدة يعطي المصفوفة B نفسها مهما كانت المصفوفة B التي لها الأبعاد الصحيحة. ‎BI = B‎ (2) لنضع المصفوفة المصفوفة I مكان المصفوفة A في العلاقة رقم (1)، بما أن A يمكن أن تكون أي مصفوفة مناسبة. ‎ZI = I‎ (3) ولنضع الآن المصفوفة Z مكان المصفوفة B في العلاقة رقم (2): ZI = Z (4) لاحظ من العلاقتين (3) و(4) أن Z و I متساويين مع الشيء نفسه، لذا يجب أن يكونا متساويين. Z = I (5) يمكنك البدء مرةً أخرى من الخطوة رقم (1) بالترتيب الآخر ‎AZ = A‎ والوصول إلى النتيجة ذاتها؛ لذا إذا وجدت مصفوفةً تعمل مثل عمل المصفوفة المحايدة، فهي بالتأكيد المصفوفة المحايدة بحد ذاتها. خاصيات أخرى للمصفوفة المحايدة لنوضح الآن بعد الخاصيات الأخرى للمصفوفة المحايدة. على سبيل المثال، يُعَد منقول المصفوفة المحايدة هو المصفوفة المحايدة نفسها: IT = I‎ وهناك خاصية أخرى مفيدة للمصفوفة المحايدة، والتي تُعَد مجرد تطبيقٍ لتعريفها؛ حيث إذا جاءت المصفوفة المحايدة بين مصفوفتين، فلن تؤثر على عملية الضرب: AIB = AB‎ كما أن مربع المصفوفة المحايدة هو المصفوفة المحايدة نفسها: I I = I‎ أو I2 = I تُعَد هذه الخاصيات واضحةً ولا حاجة لحفظها. المصفوفة القطرية Diagonal Matrix تُعَد المصفوفة القطرية (a I‎) مفيدةً أحيانًا في الرسوميات الحاسوبية. لنفترض مثلًا أننا نريد حساب الآتي، بحيث يكون a عدد حقيقي و x مصفوفة عمودية: a Ax‎‎‎ هنا وبحالات كثيرة، سنجد أنه من المفيد أحيانًا التفكير على النحو التالي: ‎(a I) Ax وبهذا تكون قد أصبحت جميع العمليات عبارة عن عمليات ضرب مصفوفات، ويُعَد ذلك ميزة، نظرًا لأنه يمكن إجراء عملية ضرب المصفوفات باستخدام عتاد الرسوميات الحاسوبية دون الحاجة لحسابها. لنحاول الآن إيجاد ناتج ضرب المصفوفتين التاليتين بأبسط طريقة كما يلي: عملية ضرب المصفوفات التي ينتج عنها المصفوفة المحايدة من الواضح أن: ‎(2 I)(0.5 I) = 2 (0.5) I = 1 I = I ولكن جرّب ضرب هاتين المصفوفتين بالطريقة العادية: لاحظ أن الصفوف من المصفوفة الأولى تتلاءم مع أعمدة المصفوفة الثانية، بحيث يساوي حاصل الجداء النقطي لها القيمة 1 لعناصر القطر الرئيسي فقط؛ بينما تساوي جميع عمليات الجداء النقطي الأخرى القيمة 0، وبالتالي فالمصفوفة الناتجة هي المصفوفة المحايدة I. معكوس المصفوفة لنفترض أن ax = b للأعداد الحقيقية a و b، وأن المتغير الحقيقي هو x، ولنوجد قيمة x الآن. إذا كان ax = b، فإن ‎(a-1) ax = (a-1)b، أو x = (a-1)b، إلّا عندما يكون العدد a صفرًا؛ فالقيمة (a-1‎) هي معكوس العدد a، وجميع الأعداد الحقيقية غير الصفرية لها معكوس. توجد فكرة مماثلة خاصة بالمصفوفات المربعة، وليكن لدينا مثلًا: Ap = q‎ لدينا: p و q هي أشعة عمودية، أما A فهي مصفوفة أبعادها n×n. ومع افتراض أن A و q معلومتان: لنجرّب معرفة قيمة المصفوفة العمودية p للوصول إلى النتيجة التالية: يُعَد إيجاد قيمة p بالتخمين أمرًا صعبًا، وسيكون الأمر أسوأ بكثير إذا كانت أبعاد المصفوفة A هي 5 × 5؛ لذا توجد طريقة أفضل لذلك، وهي باستخدام معكوس المصفوفة. إذًا، لتكن لدينا p و q مصفوفات عمودية و A مصفوفة أبعادها N×N، حيث تكون قيمة q و A معلومتين، ونريد معرفة قيمة p: ‎q = Ap‎ (1)‎ لنفترض وجود مصفوفة ‎BN×N‎ تحقق ما يلي: ‎p = Bq‎ (2) إذا وُجِدت مثل هذه المصفوفة، فيمكننا حساب ما نريده (أي المصفوفة p) من المصفوفة q، لذا عوّض ما توصلت إليه من العلاقة رقم (2) في العلاقة رقم (1): ‎q = A (Bq)‎ (3) ‎q = (AB) q‎ (4) إذا كانت العلاقة رقم (4) صحيحة، فإن ‎(AB) = I‎. وعلينا هنا أن نتذكّر أن المصفوفة المحايدة هي مصفوفة فريدة. إذا كانت المصفوفة B موجودةً فعلًا، فهي تُعَد معكوسًا للمصفوفة A، وتُكتَب بالشكل: ‎A-1‎؛ وبالتالي يمكننا الآن إيجاد قيمة p في العلاقة رقم (1) من خلال ضرب كل طرف منها في المعكوس ‎ A-1‎كما يلي: A-1 q = A-1 Ap‎ A-1 q = p‎ المصفوفة غير المفردة Non-singular ليس لجميع المصفوفات المربعة معكوس دائمًا، فالمصفوفة الصفرية مثلًا ليس لها معكوس؛ إذ لا توجد مصفوفة ‎0-1‎ تحقق الخاصية: ‎0 ‎0-1‎ = I‎، ولكن الأمر أسوأ من ذلك، فالعديد من المصفوفات المربعة التي أبعادها N × N ليس لها معكوس. تسمى المصفوفة التي لها معكوس: المصفوفة غير المفردة Non-singular، في حسن تسمى المصفوفة التي ليس لها معكوس: المصفوفة المفردة Singular. إذا كانت المصفوفة A غير مفردة، فهي تحقق ما يلي: AA-1= A-1A = I لدينا هنا مثال عن مصفوفة لها معكوس: وليكن لدينا مثلًا ما يلي: بحيث يمكننا إيجاد قيمة p باستخدام المعكوس ‎A-1‎ كما يلي: معكوس المصفوفة هو مصفوفة فريدة إذا كانت المصفوفة A غير مفردة (أي لها معكوس) وكان ‎Ap = q‎، فإن ‎p = A-1 q‎. يُعَد معكوس المصفوفة المربعة غير المفردة فريدًا، وإحدى الطرق لإثبات ذلك هي وجود مصفوفة عمودية واحدة p فقط تُعَد حلًا للمعادلة ‎Ap = q‎، لذلك يجب أن يكون هناك معكوس ‎A-1‎ واحد فقط للمصفوفة. ويُعَد المعكوس ‎A-1‎ مفيدًا في المناقشات حول المصفوفات وعمليات التحويل، ولكنه ليس مفيدًا جدًا للحسابات الفعلية، لذا لا حاجة لحسابه أبدًا. لنفترض مثلًا أن المصفوفة العمودية p تمثل نقطةً في عالم الرسوميات الحاسوبية، وأنه في حال تغير مجال الرؤية، فستُحوَّل المصفوفة العمودية إلى العلاقة ‎ .q = Ap‎ قد يفكر الكثيرون هنا بعكس التحويل باستخدام ‎A-1 q‎، ولكن هناك دائمًا طريقة أسهل للعودة إلى مجال الرؤية الأصلي بدلًا من حساب المعكوس. لنحاول إيجاد ناتج (‎AB) (B-1 A-1‎) كما يلي: باستخدام خاصية التجميع: ‎(‎AB) (B-1 A-1‎) = A (B B-1) A-1 ‎= A I A-1 = A A-1 = I خاصيات معكوس المصفوفة يساوي معكوس ضرب مصفوفتين نتيجة ضرب معكوس المصفوفة الثانية بمعكوس المصفوفة الأولى؛ فقد عكسنا ترتيب المصفوفات كما يلي: ‎(AB)-1 = B-1 A-1 وتوجد خاصية أخرى مفيدة وهي: ‎(A-1)T = (AT)-1 محدد Determinant المصفوفة المفردة يمكن حساب محدِّد المصفوفة التي أبعادها 2 × 2 على النحو التالي: إليك مثال لحساب محدّد مصفوفة: يُعَد حساب محدد المصفوفات الأكبر أكثر تعقيدًا؛ إذ يندر تطبيقها، حيث يُستخدَم المحدّد عند مناقشة المصفوفات، وليس في العمليات الحسابية. ملاحظة: يساوي محدّد المصفوفة المفردة الصفر. رتبة Rank المصفوفة إليك مثال آخر لحساب محدّد مصفوفة: للمصفوفة في المثال السابق محدّد صفري، وبالتالي فهي مصفوفة مفردة، وليس لها معكوس، وتتكون من صفين متطابقين، وبالتالي يمكن القول بأن صفوفها غير مستقلة؛ فإذا كان أحد الصفوف مضاعفًا لصف آخر، فإننا نسمّيها صفوفًا غير مستقلة؛ إذ يساوي محدّد هذه المصفوفة الصفر. وبالمثل، إذا كان أحد الأعمدة مضاعفًا لعمود آخر، فسيكون هذين العمودين غير مستقلين، وبالتالي يكون محدّد هذه المصفوفة صفرًا. رتبة المصفوفة هي الحد الأقصى لعدد الصفوف المستقلة (أو الحد الأقصى لعدد الأعمدة المستقلة)، وتكون المصفوفة المربعة ‎An×n‎ غير مفردة فقط إذا كانت رتبتها تساوي n. لنجرب إيجاد رتبة المصفوفة التالية: رتبة المصفوفة السابقة هي 3، حيث يُعَد الصف الأخير مضاعفًا للصف الأول. خاتمة بهذا نكون قد تعرفنا على مفهوم كل من المصفوفة المحايدة Identity Matrix ومعكوس المصفوفة وكيفية استخدامهما في عملية ضرب المصفوفات. وبهذا نكون قد وصلنا لنهاية هذه السلسلة التعليمية حول الأشعة وجبر المصفوفات من وجهة نظر رسومات الحاسوب، حيث غطينا عدة مواضيع عن الأشعة والمصفوفات وتعرفنا على عدة حالات للتعامل معها. ترجمة -وبتصرُّف- للفصل Identity Matrix and Matrix Inverse من كتاب Vector Math for 3D Computer Graphics لصاحبه Bradley Kjell. اقرأ أيضًا المقال السابق: ضرب المصفوفات وحسابها لعمل تصاميم ثلاثية الأبعاد 3D التعرف على النقاط والخطوط في الرسوميات الحاسوبية ثلاثية الأبعاد الأشعة والنقاط والمصفوفات العمودية في الرسوميات الحاسوبية ثلاثية الأبعاد تعرف على المصفوفات Matrices وعملياتها البسيطة في التصاميم 3D
  7. تُستخدَم عملية ضرب المصفوفات في الرسومات الحاسوبية من أجل إنشاء مصفوفات التحويل Transformation التي تُطبَّق على النقاط والأشعة. وتُصمَّم المناظر في الألعاب ذات نمط منظور الشخص الأول First Person Game مثل لعبة DOOM باستخدام النقاط والأشعة أيضًا، كما ينشأ العرض المتغير الذي نراه أثناء تحرّك اللاعب عبر هذه المناظر بتحويل تلك النقاط والأشعة. ونظرًا لأهمية المصفوفات في مجال النقاط والأشعة التي بدورها تتداخل مباشرةً مع عملية إنشاء تصاميم ثلاثية الأبعاد، فسنفصل في هذا المقال في كيفية التعامل مع المصفوفات وضربها ببعضها. لقد تحدثنا في المقال السابق عن كيفية ضرب المصفوفات بمصفوفة عمودية، ولمعرفة كيفية التعامل مع المصفوفات أكثر، سنناقش بهذا المقال مفهوم ضرب المصفوفات وخاصيتها وكيفية حسابها لعمل تصاميم ثلاثية الأبعاد 3D؛ وسنتحدث عما يلي: متى نضرب المصفوفات ناتج ضرب المصفوفات نتاج كل عنصر من عناصر المصفوفة المحصل عليها بتطبيق عملية جداء نقطي الخاصية التجميعية لضرب المصفوفات الخاصية غير التبديلية لضرب المصفوفات توزيع ضرب المصفوفات على عملية الجمع الضرب بالمصفوفة الصفرية أبعاد نتيجة ضرب المصفوفات يمكن ضرب المصفوفتين ‎A4×3‎ و ‎B3×5‎ عند تطابق البعد الداخلي الذي قيمته 3؛ فإذا رتبنا مصفوفتين مستطيلتين بحيث يكون البعد الداخلي لكل منهما متساويًا، فيمكن ضرب هاتين المصفوفتين، وستكون النتيجة عمومًا عبارة عن مصفوفة مستطيلة كالآتي: AR×N‎ BN×C‎ = ZR×C‎ تحتوي عملية الضرب AB -إذا كان تشكيلها ممكنًا- على نفس عدد صفوف المصفوفة A وعلى نفس عدد أعمدة المصفوفة B؛ وهنا إذا لم نتمكن من تجاهل البعد الداخلي، فلن نتمكن من تشكيل عملية الضرب. فلنطّلع الآن على المثال التالي، مع تجاهل كيفية حساب العناصر مبدئيًا؛ إذ سنضرب مصفوفةً أبعادها 3‎×2 في مصفوفة أخرى أبعادها 2‎×2، والنتيجة ستكون عبارة عن مصفوفة أبعادها 3‎×2: انطلاقًا مما سبق، نستطيع القول أن أبعاد نتيجة ضرب المصفوفتين ‎A4×4‎ B4×2‎ مثلًا، ستكون: A4×4‎ B4×2‎ = C4×2 ملاحظة: تُعَد المصفوفات العمودية والمصفوفات السطرية حالة خاصة من المصفوفات المستطيلة، لذا فإن قاعدة أبعاد نتيجة ضرب المصفوفات تسري عليها أيضًا. لنتدرب الآن على مزيدٍ من الأمثلة في الجدول التالي؛ إذ سنحدّد أبعاد نتيجة ضرب كلّ زوج من المصفوفات: أبعاد المصفوفة الأولى أبعاد المصفوفة الثانية النتيجة 4‎ × 3 ‫ 3‎ × 2 ‫ 4‎ x 2 2‎ × 3 ‫ 2‎ × 3 ‫ غير ممكنة 3‎ × 5 ‫ 5‎ × 1 ‫ 3‎ x 1 3‎ × 2 ‫ 2‎ × 2 ‫ 3‎ x 2 4‎ × 3 ‫ 3‎ × 1 ‫ 4‎ x 1 3‎ × 1 ‫ 1‎ × 4 ‫ 3‎ x 4 ملاحظة: سنحتاج للتحقق دائمًا من أبعاد نتيجة ضرب المصفوفات قبل إجراء العمليات الحسابية؛ فمثلًا، عملية الضرب الأخيرة في الجدول السابق غريبة بعض الشيء، ومن السهل أن يختلط علينا الأمر دون معرفة الشكل الذي يجب أن تبدو عليه النتيجة. يمكننا تشكيل عملية الضرب ‎An×m‎ Bm×p‎، ولكن لا يمكننا تشكيل عملية الضرب ‎Bm×p‎ An×m‎ إذا كانت n ≠ p. وسيتضح لاحقًا أنه حتى لو أمكن تشكيل كلتا عمليتي الضرب، فمن النادر أن يكون ‎AB = BA‎. تشكيل عملية ضرب المصفوفات يُحسَب حاصل ضرب مصفوفتين AB من خلال تطبيق الجداء النقطي على كل صف من المصفوفة A مع كل عمود من المصفوفة B، بحيث يكون عدد الأعمدة في المصفوفة A مساويًا لعدد الصفوف في المصفوفة B. لنجرب قلب أحد أعمدة المصفوفة B بحيث تصبح عناصره محاذيةً لصفوف المصفوفة A، ثم نطبّق عليها الجداء النقطي؛ وهنا سينتج لدينا العنصر الموجود في الصف 1 والعمود 1 من المصفوفة الناتجة. لننتقل بعدها إلى الصف التالي مع تطبيق الجداء النقطي، لينتج لدينا العنصر الموجود في الصف 2 والعمود 1 من المصفوفة الناتجة؛ وسنستمر بهذه العملية حتى الوصول إلى الصف الأخير. لننتقل الآن إلى العمود التالي ونقوم بنفس الشيء عند الانتهاء من العمود الأول من المصفوفة B، ونواصل تطبيق ذات الشيء على كل عمود من المصفوفة B حتى حساب جميع العناصر. ملاحظة: العنصر ij من المصفوفة الناتجة = الجداء النقطي للصف i من المصفوفة A مع العمود j من المصفوفة B. لنفترض أننا نشكّل عملية الضرب التالية: ‎A5×3‎ B3×2 = C5×2 سنستخدم الصف الثالث من المصفوفة A والعمود الثاني من المصفوفة B، لحساب الصف الثالث، والعمود الثاني من المصفوفة الناتجة C. طريقة أخرى لتصور عملية ضرب المصفوفات قد تعرض كتب الرسوميات الحاسوبية طريقةً مختلفةً لضرب المصفوفات؛ إذ أن هناك عدة طرق للتفكير في الأمر بالرغم من أن النتائج هي نفسها. يوضّح الرسم البياني التالي مثلًا طريقةً أخرى لتصور عملية الضرب ‎AB = C‎؛ إذ سنكتب المصفوفة A على يسار المصفوفة الناتجة C والمصفوفة B فوقها، ثم نرسم خطوطًا أفقية وأخرى عمودية لتقسيم المصفوفة C إلى خلايا؛ وسيؤدي ذلك تلقائيًا إلى تشكيل العدد الصحيح من الخلايا. ينتج العدد الذي يجب وضعه في كل خلية من حاصل الجداء النقطي لصف المصفوفة A مع عمود المصفوفة B اللذين يتقاطعان عند هذه الخلية. لنحسب الآن قيمة العنصرين c11‎ و c32‎ كما يلي: c11 = 1×4 + -2×-1 = 6 c32 = -1×1 + 4×2 = 7 سنجرّب الآن حساب حاصل الجداء النقطي الخاص بكل خلية في الرسم البياني التالي: وستكون المصفوفة الناتجة هي المصفوفة التالية: عناصر المصفوفة هي أعداد حقيقية يُعَد كل عنصر في المصفوفة مقدارًا سلميًا Scalar أي عددًا حقيقيًا. لقد استخدمنا في أمثلتنا السابقة أعدادًا صحيحة حتى تكون العملية الحسابية سهلة، ولكن إليك مثال آخر مع قيم عشرية: تشتمل معظم برامج جداول البيانات والآلات الحاسبة الإلكترونية العلمية على دالات رياضية خاصة بالمصفوفات؛ لكن من الأفضل محاولة إجراء الحسابات ذاتيًا عند التعلم من أجل استيعاب العملية. عملية ضرب المصفوفات ليست عملية تبديلية لا تُعَد المساواة ‎AB= BA‎ صحيحةً دائمًا، فحتى إذا تمكنا من تشكيل هاتين العمليتين، فمن النادر أن تكون نتيجتهما متساويتين. لنحسب مثلًا عمليتي الضرب التاليتين: العملية النتيجة الناتج الناتج وهذا يوضّح أن ‎ AB ≠ BA‎بصورة عامة بالنسبة للمصفوفتين A و B، ولكن يمكن مثلًا: A4×4 04×4 = 04×4 A4×4 = 04×4 المصفوفة 0 هي المصفوفة الصفرية ذات الأبعاد الصحيحة لجعل عملية الضرب ممكنة، وبالتالي يمكن أن تكون عملية الضرب عمليةً تبديليةً ‎AB = BA‎ بالنسبة لبعض المصفوفات؛ ولكن لا يمكننا تعميم هذه الخاصية على جميع المصفوفات. تدريب عملي لنوجد ناتج ضرب المصفوفتين التاليتين: لنجرب أيضًا ضرب المصفوفتين التاليتين مثلًا: استخراج عامل عددي من مصفوفة ليكن c(AB) = (cA)B‎ من أجل العدد الحقيقي c، علمًا أن أن c A‎ يعني أن كل عنصر من عناصر المصفوفة A مضروب بالعدد c. سنوضّح هذه القاعدة في المثال التالي: يمكننا تبسيط عملية ضرب المصفوفات في كثير من الأحيان من خلال استخراج عامل عددي من إحدى المصفوفات كما في المثال التالي: عملية ضرب المصفوفات هي عملية تجميعية لنضرب المصفوفتين الأوليتين، ثم نضرب النتيجة بالمصفوفة الثالثة كما يلي: ولنبدأ الآن بالمصفوفتين الأخيرتين أولًا كما يلي: وكما هو واضح، الإجابة النهائية هي نفسها بكلا الطريقتين، وهذا يوضّح أن عملية ضرب المصفوفات هي عملية تجميعية Associative: (AB) ‪C = A (BC) يجب أن تكون الأبعاد الداخلية للمصفوفتين A و B نفسها، كما يجب أن تكون الأبعاد الداخلية للمصفوفتين B و C نفسها. تُكتَب عملية ضرب ثلاث مصفوفات في العادة بالشكل الآتي: ABC. إذًا لنفترض مثلًا: A5×5 B?×? C3×4 = D?×?‪ حيث تكون أبعاد المصفوفتين B و D كما يلي: ‪A5×5 B5×3 C3×4 = D5×4 خاصية توزيع ضرب المصفوفات في جمعها تتعامل خاصية التوزيع مع تعبيرٍ يحتوي على كلٍّ من عمليتي ضرب وجمع المصفوفات؛ وسنتبع الخطوات التالية لذلك. أولًا، نجمع المصفوفتين كما يلي: ثانيًا، نضرب المصفوفتين: ثالثًا، نجري العملية مرةً أخرى بترتيب مختلف؛ إذ سنجري أولًا عملية ضرب المصفوفة اليسرى بكل من المصفوفتين الأخريين كما يلي: في الأخير، نجمع ناتج عمليتي الضرب السابقتين: وكما نلاحظ، كلتا النتيجتين متماثلتان، مما يدل على أن ضرب المصفوفات توزيعيٌّ على جمعها: ‪A (B + C) = AB + AC ويمكن القول أيضًا أن: (X + Y) Z = XZ + YZ ملاحظة: استخدمنا في معظم أمثلتنا أعدادًا صحيحة، ولكن لا يجب أن ننسى أن عناصر المصفوفة قد تكون أعددًا حقيقية أو متغيرات كما يلي: ملخص بالقواعد الخاصة بعملية ضرب المصفوفات سنذكر الآن قائمة بالقواعد الخاصة بعملية ضرب المصفوفات التي ناقشناها في هذا المقال، حيث تفترض كل قاعدة أنه يمكن ضرب المصفوفات؛ أي أن أبعادها مناسبة لعملية الضرب: يمكن تشكيل عملية ضرب المصفوفات في حال: إذا كان ‎AM×K BK×N = C‎، فإن C = CM×N‎ ضرب المصفوفات ليس عملية تبديلية: ‎AB ≠ BA‎، إلّا في حالات ناردة ضرب المصفوفات هو عملية تجميعية: A (BC) = (AB) C = ABC استخراج المعامل العددي من المصفوفات: a(AB) = (a A) B = a AB = A (a B) خاصية التوزيع على الجمع: A ( B + C) = AB + AC (A + B) C = AC + BC الضرب بالمصفوفة الصفرية 0: 0A = 0 بهذا نكون قد وصلنا إلى نهاية هذا المقال التي تعرّفنا من خلاله على عملية ضرب المصفوفات وحسابها، وسنناقش في المقال التالي مزيدًا من خاصيات عملية ضرب المصفوفة بمصفوفة أخرى. ترجمة -وبتصرُّف- للفصل Matrix-Matrix Multiplication من كتاب Vector Math for 3D Computer Graphics لصاحبه Bradley Kjell. اقرأ أيضًا المقال السابق: ضرب المصفوفات بمصفوفة عمودية في التصاميم ثلاثية الأبعاد 3D التعرف على النقاط والخطوط في الرسوميات الحاسوبية ثلاثية الأبعاد الأشعة والنقاط والمصفوفات العمودية في الرسوميات الحاسوبية ثلاثية الأبعاد تعرف على المصفوفات Matrices وعملياتها البسيطة في التصاميم 3D
  8. حان الوقت الآن لإنشاء تطبيق مدونة متكامل باستخدام إطار عمل جانغو Django، فقد تعلمنا في المقال السابق كيف يمكن للنماذج Model والعروض View والقوالب Template أن تعمل معًا لإنشاء تطبيق جانغو لكن هذه العملية صعبة نوعًا ما، إذ يتوجب علينا كتابة 5 إجراءات على الأقل لكل ميزة نريد تحقيقها، وستكون الشيفرة البرمجية مكررةً في معظمها. لذا سنستخدم في هذا المقال واحدة من أفضل ميزات جانغو، وهي لوحة المدير Admin Panel المُضمَّنة، فما عليك سوى كتابة إجراء العرض لمعظم الميزات التي ترغب في إنشائها لتطبيقك، وسيتولى جانغو الباقي نيابة عنك تلقائيًا. إنشاء طبقة النموذج Model سنبدأ أولًا بتصميم بنية قاعدة البيانات. تصميم بنية قاعدة البيانات بالنسبة لأيّ نظام تدوين أساسي، ستحتاج إلى 4 نماذج على الأقل هي User و Category و Tag و Post، وسنضيف في المقال التالي بعض الميزات المتقدمة لاحقًا، ولكن سنكتفي حاليًا بهذه النماذج الأربعة فقط. نموذج المستخدم User المفتاح نوعه معلومات عنه المعرّف id عدد صحيح Integer يتزايد تلقائيًا الاسم name سلسلة نصية String - البريد الإلكتروني email سلسلة نصية فريد كلمة السر password سلسلة نصية - إن النموذج User مُضمَّن مسبقًا في جانغو، إذ يوفّر هذا النموذج المُضمَّن بعض الميزات الأساسية مثل تشفير كلمات المرور Password Hashing واستثياق المستخدمين User Authentication، بالإضافة إلى نظام الأذونات المُضمَّن مع مدير جانغو Django Admin كما سنوضّح لاحقًا. نموذج الفئة Category المفتاح نوعه معلومات عنه المعرّف id عدد صحيح Integer يتزايد تلقائيًا الاسم name سلسلة نصية String - الاسم المختصر slug سلسلة نصية فريد الوصف description نص - نموذج الوسم Tag المفتاح نوعه معلومات عنه المعرّف id عدد صحيح Integer يتزايد تلقائيًا الاسم name سلسلة نصية String - الاسم المختصر slug سلسلة نصية فريد الوصف description نص - نموذج المنشور Post المفتاح نوعه معلومات عنه المعرّف id عدد صحيح Integer يتزايد تلقائيًا العنوان title سلسلة نصية String - الاسم المختصر slug سلسلة نصية فريد المحتوى content نص - featured_image سلسلة نصية String - is_published قيمة منطقية Boolean - is_featured قيمة منطقية - created_at تاريخ Date - نموذج الموقع Site ستحتاج أيضًا إلى جدول آخر لتخزين المعلومات الأساسية للموقع بالكامل مثل الاسم والوصف والشعار. المفتاح نوعه معلومات عنه الاسم name سلسلة نصية String - الوصف description نص text - الشعار logo سلسلة نصية - العلاقات توجد ست علاقات بالنسبة لهذا التطبيق وهي: لكل مستخدم عدة منشورات لكل فئة عدة منشورات ينتمي كل وسم إلى منشورات متعددة ينتمي كل منشور إلى مستخدم واحد ينتمي كل منشور إلى فئة واحدة ينتمي كل منشور إلى وسوم متعددة تطبيق التصميم باستخدام جانغو الآن، بعد أن انتهينا من تصميم هذا النموذج، سنبدأ في تطبيقه كما سنوضح في الخطوات التالية نموذج الموقع Site لنبدأ بنموذج الموقع Site مع توفير خصائص لحفظ بيانات الموقع مثل اسم الموقع ووصفه وصورة الشعار كما يلي: class Site(models.Model): name = models.CharField(max_length=200) description = models.TextField() logo = models.ImageField(upload_to="logo/") class Meta: verbose_name_plural = "site" def __str__(self): return self.name لاحظ أن نوع حقل الصورة ImageField()‎ هو سلسلة نصية string، إذ لا يمكن لقواعد البيانات تخزين الصور، لذا تُخزَّن الصور في نظام ملفات خادمك، وسيحتفظ هذا الحقل بالمسار الذي يشير لموقع الصورة. سنرفع الصور في هذا المثال إلى المجلد ‎mediafiles/logo/‎، وتذكّر إضافة مجلد ملفات الوسائط ‎MEDIA_ROOT = "mediafiles/"‎ في الملف settings.py الذي أعددناه سابقًا. ملاحظة: تحتاج لأن تثبّت مكتبة Pillow على جهازك ليعمل الحقل ImageField()‎ كما يلي: pip install Pillow نموذج الفئة Category بعدها سنصميم نموذج الفئة Category الذي سيُستخدم لتنظيم المحتويات ضمن المدونة ضمن فئات ويحدد الحقول التي سنحتاج إليها لتمثيل الفئات بشكل صحيح في قاعدة البيانات. يتضمن نموذج الفئة Category المحتويات التالية: class Category(models.Model): name = models.CharField(max_length=200) slug = models.SlugField(unique=True) description = models.TextField() class Meta: verbose_name_plural = "categories" def __str__(self): return self.name ينبغي أن يكون النموذج Category مفهومًا بالنسبة لك، لكن دعنا نوضّح الصنف Meta، الذي سنستخدمه لإضافة البيانات الوصفية Metadata إلى النماذج. تمثّل البيانات الوصفية للنموذج أيّ شيء ليس حقلًا مثل خيارات الترتيب واسم جدول قاعدة البيانات وما إلى ذلك، حيث استخدمنا في مثالنا الخيار verbose_name_plural لتحديد صيغة الجمع لكلمة "category"، فجانغو ليس ذكيًا مثل لارافيل Laravel في هذا الجانب، فإن لم نمنح جانغو صيغة الجمع الصحيحة، فسوف يستخدم الجمع "categorys"، وهذا خاطئ. تحدد الدالة ‎__str__(self)‎ الحقل الذي سيستخدمه جانغو عند الإشارة إلى فئة معينة، حيث استخدمنا في مثالنا الحقل name، وسنوضّح أهمية ذلك عندما نصل إلى قسم مدير جانغو. نموذج الوسم Tag يتضمن نموذج الوسم Tag لتخزين الوسوم التي سترتبط بالمنشورات وهو يتضمن اسم الوسم والرابط اللطيف له ووصفه كما توضح الشيفرة التالية: class Tag(models.Model): name = models.CharField(max_length=200) slug = models.SlugField(unique=True) description = models.TextField() def __str__(self): return self.name نموذج المنشور Post لنعرف أخيرًا نموذج المنشور Post الذي يتضمن اسم المنشور والرابط اللطيف والمحتوى الرئيسي والصورة البارزة للمنشور وتاريخ إنشائه وحقلين منطقيين يوضحان هل نشر أم لا وهل هو منشور مميز أما لا كما توضح الشيفرة التالية: from ckeditor.fields import RichTextField . . . class Post(models.Model): title = models.CharField(max_length=200) slug = models.SlugField(unique=True) content = RichTextField() featured_image = models.ImageField(upload_to="images/") is_published = models.BooleanField(default=False) is_featured = models.BooleanField(default=False) created_at = models.DateField(auto_now=True) # تعريف العلاقات category = models.ForeignKey(Category, on_delete=models.CASCADE) tag = models.ManyToManyField(Tag) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) def __str__(self): return self.title لاحظ في السطر 1 أنه إذا نسختَ هذه الشيفرة البرمجية ولصقتها لديك، فسيخبرك محرّر نصوصك بعدم العثور على RichTextField و ckeditor، لأنها حزمة خارجية ولا تُضمَّن في إطار عمل جانغو. تذكّر أنه يمكنك فقط إضافة نص عادي عند إنشاء منشور في المقال السابق، وهذا ليس مثاليًا لمقال في مدونة. إذ تمنحك محرّرات النصوص الغنية أو محرّرات WYSIWYG القدرة على تحرير صفحات HTML مباشرةً دون كتابة الشيفرة البرمجية، حيث استخدمنا المحرّر CKEditor في هذا المقال. لذا يمكنك تثبيت المحرّر CKEditor من خلال تشغيل الأمر التالي: pip install django-ckeditor سجّل بعد ذلك ckeditor في ملف الإعدادات settings.py كما يلي: INSTALLED_APPS = [ "blog", "ckeditor", "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", ] تعريف العلاقات أخيرًا، يمكنك إضافة علاقات إلى النماذج، حيث سنضيف الأسطر الثلاثة التالية في النموذج Post للقيام بذلك كما يلي: category = models.ForeignKey(Category, on_delete=models.CASCADE) tag = models.ManyToManyField(Tag) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) بما أننا نستخدم نموذج المستخدم User المُضمَّن (settings.AUTH_USER_MODEL)، فتذكر استيراد الوحدة settings كما يلي: from django.conf import settings ولِّد بعد ذلك ملفات التهجير Migration Files وطبّقها على قاعدة البيانات باستخدام الأمرين التاليين: python manage.py makemigrations python manage.py migrate إعداد لوحة مدير المدونة الخطوة التالية هي إعداد لوحة المدير، حيث يأتي جانغو مع نظام إدارة مضمَّن معه، ويمكنك استخدامه من خلال التسجيل بمستخدم Superuser باستخدام الأمر التالي: python manage.py createsuperuser يمكنك بعد ذلك الوصول إلى لوحة المدير من خلال الانتقال إلى العنوان ‎http://127.0.0.1:8000/admin/‎. لا تزال لوحة المدير فارغة حاليًا، ولا يوجد بها سوى تبويب الاستيثاق Authentication الذي يمكنك استخدامه لإسناد أدوار مختلفة للمستخدمين، وهذا أمر معقد إلى حد ما ويتطلب مقالًا آخر، لذا لن نتحدث عنه الآن، بل سنركز على كيفية ربط تطبيق المدونة blog بنظام المدير، حيث يجب أن تجد ملفًا بالاسم admin.py ضمن التطبيق blog، لذا أضف إليه الشيفرة البرمجية التالية: from django.contrib import admin from .models import Site, Category, Tag, Post # سجّل نماذجك هنا class CategoryAdmin(admin.ModelAdmin): prepopulated_fields = {"slug": ("name",)} class TagAdmin(admin.ModelAdmin): prepopulated_fields = {"slug": ("name",)} class PostAdmin(admin.ModelAdmin): prepopulated_fields = {"slug": ("title",)} admin.site.register(Site) admin.site.register(Category, CategoryAdmin) admin.site.register(Tag, TagAdmin) admin.site.register(Post, PostAdmin) نستورد في السطر 2 النماذج التي أنشأناها، ثم نسجل النماذج المستوردة باستخدام التابع admin.site.register()‎. لاحظ بعد ذلك وجود شيء إضافي عند تسجيل النموذج Category، وهو الصنف CategoryAdmin الذي عرّفناه في السطر 6، وبالتالي يمكنك تمرير بعض المعلومات الإضافية إلى نظام الإدارة في جانغو. يمكنك استخدام الميزة prepopulated_fields لتوليد أسماء لطيفة Slugs لجميع الفئات والوسوم والمنشورات، حيث تعتمد قيمة slug على الاسم name، ولنختبر ذلك من خلال إنشاء فئة جديدة. انتقل إلى العنوان ‎http://127.0.0.1:8000/admin/‎، وانقر على الفئات Categories، وأضف فئة جديدة. تذكّر أننا حددنا صيغة الجمع للكلمة Category في نموذجنا، فإن لم نفعل ذلك، فسيستخدم جانغو الجمع Categorys بدلًا من ذلك كما شرحنا سابقًا. لاحظ توليد الاسم اللطيف تلقائيًا عند كتابة الاسم. حاول الآن إضافة بعض البيانات التجريبية، يجب أن يعمل كل شيء بنجاح. عمليات ضبط اختيارية Optional Configurations لم ينتهي عملنا بعد، لذا افتح لوحة الفئات، ستلاحظ أن بإمكانك الوصول إلى الفئات من صفحة المنشور، ولكن لا توجد طريقة للوصول إلى المنشورات المقابلة من صفحة الفئة. لحل هذه المشكلة، يمكنك استخدام الصنف InlineModelAdmin في الملف blog/admin.py كما يلي: class PostInlineCategory(admin.StackedInline): model = Post max_num = 2 class CategoryAdmin(admin.ModelAdmin): prepopulated_fields = {"slug": ("name",)} inlines = [ PostInlineCategory ] أنشأنا أولًا الصنف PostInlineCategory، ثم استخدمناه في CategoryAdmin، وتعني عبارة max_num = 2 عرض منشورين فقط في صفحة الفئة التي ستبدو كما يلي: يمكنك بعد ذلك تطبيق الشيء نفسه مع TagAdmin في الملف blog/admin.py: class PostInlineTag(admin.TabularInline): model = Post.tag.through max_num = 5 class TagAdmin(admin.ModelAdmin): prepopulated_fields = {"slug": ("name",)} inlines = [ PostInlineTag ] لاحظ أن هذه الشيفرة البرمجية مشابهة جدًا لما سبق، ولكن المتغير model ليس Post فقط، بل هو Post.tag.through، لأن العلاقة بين المنشور Post والوسم Tag هي علاقة متعدد إلى متعدد Many-to-Many، وستكون النتيجة النهائية كما يلي: بناء طبقة العروض View ركزنا في الأقسام السابقة على الواجهة الخلفية ولوحة مدير المدونة، وسنركز الآن على جزء الواجهة الأمامية، وهو الجزء الذي يمكن للمستخدمين رؤيته، حيث سنبدأ بدوال العرض View Functions. بما أننا أعددنا لوحة المدير لمدونتنا، فلن نحتاج إلى إنشاء عمليات CRUD الكاملة، وبالتالي وسنهتم فقط بكيفية استرداد المعلومات من قاعدة البيانات. سنحتاج إلى أربع صفحات هي: الصفحة الرئيسية وصفحة الفئة وصفحة الوسم وصفحة المنشور، وسنحتاج إلى دالة عرض واحدة لكلٍّ منها. عرض الصفحة الرئيسية home ضع المحتويات التالية في الملف blog/views.py: from .models import Site, Category, Tag, Post def home(request): site = Site.objects.first() posts = Post.objects.all().filter(is_published=True) categories = Category.objects.all() tags = Tag.objects.all() return render(request, 'home.html', { 'site': site, 'posts': posts, 'categories': categories, 'tags': tags, }) نستورد في السطر 1 النماذج التي أنشأناها في المقال السابق. يحتوي الموقع في السطر 4 على المعلومات الأساسية لموقعنا، حيث نسترد دائمًا السجل الأول من قاعدة البيانات. يضمن التابع filter(is_published=True)‎ في السطر 5 عرض المقالات المنشورة فقط. بعد ذلك علينا تجهيز موجّه إرسال Dispatcher لعنوان URL المقابل لهذه الصفحة في الملف djangoBlog/urls.py كما يلي: path('', views.home, name='home'), عرض الفئة category ضع أيضًا المحتويات التالية في الملف blog/views.py: def category(request, slug): site = Site.objects.first() posts = Post.objects.filter(category__slug=slug).filter(is_published=True) requested_category = Category.objects.get(slug=slug) categories = Category.objects.all() tags = Tag.objects.all() return render(request, 'category.html', { 'site': site, 'posts': posts, 'category': requested_category, 'categories': categories, 'tags': tags, }) وتذكر تجهيز موجّه إرسال عنوان URL المقابل لهذه الصفحة في الملف djangoBlog/urls.py كما يلي: path('category/<slug:slug>', views.category, name='category'), مرّرنا متغيرًا إضافيًا هو slug من عنوان URL إلى دالة العرض، واستخدمنا هذا المتغير في السطرين 3 و 4 من الشيفرة البرمجية السابقة للعثور على الفئة والمنشورات الصحيحة. عرض الوسم tag ضع أيضًا المحتويات التالية في الملف blog/views.py: def tag(request, slug): site = Site.objects.first() posts = Post.objects.filter(tag__slug=slug).filter(is_published=True) requested_tag = Tag.objects.get(slug=slug) categories = Category.objects.all() tags = Tag.objects.all() return render(request, 'tag.html', { 'site': site, 'posts': posts, 'tag': requested_tag, 'categories': categories, 'tags': tags, }) تذكر تجهيز موجّه إرسال عنوان URL المقابل لهذه الصفحة في الملف djangoBlog/urls.py كما يلي: path('tag/<slug:slug>', views.tag, name='tag'), عرض المنشور post ضع أيضًا المحتويات التالية في الملف blog/views.py: def post(request, slug): site = Site.objects.first() requested_post = Post.objects.get(slug=slug) categories = Category.objects.all() tags = Tag.objects.all() return render(request, 'post.html', { 'site': site, 'post': requested_post, 'categories': categories, 'tags': tags, }) وتذكر تجهيز موجّه إرسال عنوان URL المقابل لهذه الصفحة في الملف djangoBlog/urls.py كما يلي: path('post/<slug:slug>', views.post, name='post'), إنشاء طبقة القوالب Template يمكنك استخدام قالب جاهز، فنحن في هذه السلسة لا نركز على لغتي HTML و CSS. وستكون بنية القالب الذي سنستخدمه كما يلي: templates ├── category.html ├── home.html ├── layout.html ├── post.html ├── search.html ├── tag.html └── vendor ├── list.html └── sidebar.html يحتوي الملف layout.html على ترويسة وتذييل الصفحة، وهو المكان الذي نستورد فيه عادة أكواد التنسيقات CSS وأكواد جافا سكريبت JavaScript. وتوجهنا دوال العرض إلى القوالب home و category و tag و post، وتتوسّع جميعها إلى القالب layout الذي يعتبر كأساس لها جميعًا. وتتواجد المكونات التي ستظهر عدة مرات في قوالب مختلفة ضمن المجلد vendor، ويمكنك استيرادها باستخدام الوسم include. قالب التخطيط Layout ضع المحتويات التالية في الملف layout.html: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> {% load static %} <link rel="stylesheet" href="{% static 'style.css' %}" /> {% block title %}{% endblock %} </head> <body class="container mx-auto font-serif"> <nav class="flex flex-row justify-between h-16 items-center border-b-2"> <div class="px-5 text-2xl"> <a href="/"> My Blog </a> </div> <div class="hidden lg:flex content-between space-x-10 px-10 text-lg"> <a href="https://github.com/ericnanhu" class="hover:underline hover:underline-offset-1" >GitHub</a > <a href="#" class="hover:underline hover:underline-offset-1">Link</a> <a href="#" class="hover:underline hover:underline-offset-1">Link</a> </div> </nav> {% block content %}{% endblock %} <footer class="bg-gray-700 text-white"> <div class="flex justify-center items-center sm:justify-between flex-wrap lg:max-w-screen-2xl mx-auto px-4 sm:px-8 py-10" > <p class="font-serif text-center mb-3 sm:mb-0"> Copyright © <a href="https://www.ericsdevblog.com/" class="hover:underline" >Eric Hu</a > </p> <div class="flex justify-center space-x-4"> . . . </div> </div> </footer> </body> </html> لاحظ في السطرين 7 و 8 الطريقة التي يمكنك بها استيراد الملفات الساكنة ملفات CSS و جافا سكريبت في جانغو، فكما وضحنا لن نتحدث في هذا المقال عن لغة CSS، ولكن ما يهمنا هو معرفة طريقة استيراد ملفات CSS إضافية لتطبيق جانغو. سيبحث جانغو عن الملفات الساكنة Static Files في مجلدات التطبيق الفردية افتراضيًا، حيث سيذهب إلى المجلد ‎/blog‎ في تطبيقنا blog، ويبحث عن المجلد static، ثم يبحث عن الملف style.css ضمن المجلد static كما هو محدد في القالب. blog ├── admin.py ├── apps.py ├── __init__.py ├── migrations ├── models.py ├── static │ ├── input.css │ └── style.css ├── tests.py └── views.py قالب الصفحة الرئيسية Home ضع المحتويات التالية في الملف home.html: {% extends 'layout.html' %} {% block title %} <title>Page Title</title> {% endblock %} {% block content %} <div class="grid grid-cols-4 gap-4 py-10"> <div class="col-span-3 grid grid-cols-1"> <!-- المنشور البارز --> <div class="mb-4 ring-1 ring-slate-200 rounded-md hover:shadow-md"> <a href="{% url 'post' featured_post.slug %}" ><img class="float-left mr-4 rounded-l-md object-cover h-full w-1/3" src="{{ featured_post.featured_image.url }}" alt="..." /></a> <div class="my-4 mr-4 grid gap-2"> <div class="text-sm text-gray-500"> {{ featured_post.created_at|date:"F j, o" }} </div> <h2 class="text-lg font-bold">{{ featured_post.title }}</h2> <p class="text-base"> {{ featured_post.content|striptags|truncatewords:80 }} </p> <a class="bg-blue-500 hover:bg-blue-700 rounded-md p-2 text-white uppercase text-sm font-semibold font-sans w-fit focus:ring" href="{% url 'post' featured_post.slug %}" >Read more →</a > </div> </div> {% include "vendor/list.html" %} </div> {% include "vendor/sidebar.html" %} </div> {% endblock %} لاحظ أننا فصلنا الشريط الجانبي وقائمة المنشورات ووضعناها في المجلد vendor بدلًا من كتابة شيفرتها البرمجية الثابتة، إذ سنستخدم المكونات نفسها في صفحة الفئة والوسم. قائمة المنشورات ضع أيضًا المحتويات التالية في الملف vendor/list.html: <!-- قائمة المنشورات --> <div class="grid grid-cols-3 gap-4"> {% for post in posts %} <!-- المنشور --> <div class="mb-4 ring-1 ring-slate-200 rounded-md h-fit hover:shadow-md"> <a href="{% url 'post' post.slug %}" ><img class="rounded-t-md object-cover h-60 w-full" src="{{ post.featured_image.url }}" alt="..." /></a> <div class="m-4 grid gap-2"> <div class="text-sm text-gray-500"> {{ post.created_at|date:"F j, o" }} </div> <h2 class="text-lg font-bold">{{ post.title }}</h2> <p class="text-base"> {{ post.content|striptags|truncatewords:30 }} </p> <a class="bg-blue-500 hover:bg-blue-700 rounded-md p-2 text-white uppercase text-sm font-semibold font-sans w-fit focus:ring" href="{% url 'post' post.slug %}" >Read more →</a > </div> </div> {% endfor %} </div> لاحظ في الأسطر من 3 إلى 27 كيف مرّرنا المتغير posts من العرض إلى القالب، حيث يحتوي هذا المتغير على مجموعة من المنشورات، وسنقوم بالمرور ضمن القالب على كل عنصر في هذه المجموعة باستخدام حلقة for. تذكّر في السطر 6 أننا أنشأنا موجّه إرسال عنوان URL كما يلي: path('post/<slug:slug>', views.post, name='post'), ستجد التعليمة ‎{% url 'post' post.slug %}‎ في القالب موجّهَ إرسال عنوان URL بالاسم 'posts'، وتسند القيمة post.slug إلى المتغير ‎<slug:slug>‎، والذي سيُمرَّر بعد ذلك إلى دالة العرض المقابلة. ينسّق المرشّح Filter الذي هو date في السطر 14 بيانات التاريخ المُمرَّرة إلى القالب لأن القيمة الافتراضية ليست سهلة الاستخدام، ويمكنك العثور على تنسيقات تواريخ أخرى في توثيق جانغو الرسمي. وضعنا مرشحَين بعد post.content في السطر 18، حيث يزيل المرشّح الأول وسوم HTML، ويأخذ المرشّح الثاني أول 30 كلمة ويقتطع الباقي. قالب الشريط الجانبي Sidebar ضع أيضًا المحتويات التالية في الملف vendor/sidebar.html: <div class="col-span-1"> <div class="border rounded-md mb-4"> <div class="bg-slate-200 p-4">Search</div> <div class="p-4"> <form action="" method="get"> <input type="text" name="search" id="search" class="border rounded-md w-44 focus:ring p-2" placeholder="Search something..."> <button type="submit" class="bg-blue-500 hover:bg-blue-700 rounded-md p-2 text-white uppercase font-semibold font-sans w-fit focus:ring">Search</button> </form> </div> </div> <div class="border rounded-md mb-4"> <div class="bg-slate-200 p-4">Categories</div> <div class="p-4"> <ul class="list-none list-inside"> {% for category in categories %} <li> <a href="{% url 'category' category.slug %}" class="text-blue-500 hover:underline" >{{ category.name }}</a > </li> {% endfor %} </ul> </div> </div> <div class="border rounded-md mb-4"> <div class="bg-slate-200 p-4">Tags</div> <div class="p-4"> {% for tag in tags %} <span class="mr-2" ><a href="{% url 'tag' tag.slug %}" class="text-blue-500 hover:underline" >{{ tag.name }}</a ></span > {% endfor %} </div> </div> <div class="border rounded-md mb-4"> <div class="bg-slate-200 p-4">More Card</div> <div class="p-4"> <p> . . . </p> </div> </div> </div> قالب الفئة Category ضع المحتويات التالية في الملف category.html: {% extends 'layout.html' %} {% block title %} <title>Page Title</title> {% endblock %} {% block content %} <div class="grid grid-cols-4 gap-4 py-10"> <div class="col-span-3 grid grid-cols-1"> {% include "vendor/list.html" %} </div> {% include "vendor/sidebar.html" %} </div> {% endblock %} قالب الوسم Tag ضع المحتويات التالية في الملف tag.html: {% extends 'layout.html' %} {% block title %} <title>Page Title</title> {% endblock %} {% block content %} <div class="grid grid-cols-4 gap-4 py-10"> <div class="col-span-3 grid grid-cols-1"> {% include "vendor/list.html" %} </div> {% include "vendor/sidebar.html" %} </div> {% endblock %} قالب المنشور Post أخيرًا، يتضمن قالب المنشور المحتويات التالية: {% extends 'layout.html' %} {% block title %} <title>Page Title</title> {% endblock %} {% block content %} <div class="grid grid-cols-4 gap-4 py-10"> <div class="col-span-3"> <img class="rounded-md object-cover h-96 w-full" src="{{ post.featured_image.url }}" alt="..." /> <h2 class="mt-5 mb-2 text-center text-2xl font-bold">{{ post.title }}</h2> <p class="mb-5 text-center text-sm text-slate-500 italic">By {{ post.user|capfirst }} | {{ post.created_at }}</p> <div>{{ post.content|safe }}</div> <div class="my-5"> {% for tag in post.tag.all %} <a href="{% url 'tag' tag.slug %}" class="text-blue-500 hover:underline" mr-3">#{{ tag.name }}</a> {% endfor %} </div> </div> {% include "vendor/sidebar.html" %} </div> {% endblock %} لاحظ في السطر 19 أننا أضفنا المرشح safe، لأن جانغو سيعرض شيفرة HTML كنص عادي افتراضيًا لأسباب أمنية، لذا يجب ان نخبر جانغو بأنه يمكن عرض شيفرات HTML كما هي. أصبح كل شيءجاهزًا، كل ما عليك الأن هو تشغيل خادم التطوير باستخدام الأمر التالي، وعرض تطبيق جانغو الأول الخاص بك: python manage.py runserver ترجمة -وبتصرّف- للمقال Django for Beginners #4 - The Blog App لصاحبه Eric Hu. اقرأ أيضًا المقال السابق: استخدام عمليات CRUD لإدارة مدونة في جانغو إنشاء تطبيق جانغو وتوصيله بقاعدة بيانات إنشاء موقع ويب هيكلي بجانغو رفع مستوى أمان تطبيقات جانغو في بيئة الإنتاج البدء مع إطار العمل جانغو لإنشاء تطبيق ويب
  9. قدمنا في المقالات السابقة من هذه السلسلة العديد من المفاهيم الجديدة في جانغو، وسنوضح في هذا المقال كيفية تفاعل الموجه URL Dispatcher والنماذج Models والعروض Views والقوالب Templates معًا في تطبيق مدونة في جانغو. لن ننشئ في هذا المقال مدونة كاملة الميزات مع الفئات Categories والوسوم Tags وما إلى ذلك، بل سنكتفي فقط بإنشاء صفحة تعرض مقالًا منشورًا، وصفحة رئيسية تعرض قائمةً بجميع المقالات، وصفحة لإنشاء وتعديل وحذف المنشورات. تصميم بنية قاعدة البيانات لنبدأ أولًا بطبقة النموذج Model التي سنصمم فيها بنية قاعدة البيانات، لذا انتقل إلى الملف blog/models.py وأنشئ نموذج Post جديد، وضع فيه الشيفرة البرمجية التالية: from django.db import models class Post(models.Model): title = models.CharField(max_length=100) content = models.TextField() يحتوي نموذج المنشور Post على حقلين فقط هما، العنوان title من النوع CharField بحد أقصى 100 محرف، والمحتوى content من النوع TextField. لتطبيق هذه التغييرات على قاعدة البيانات علينا توليد ملفات التهجير Migration المقابلة باستخدام الأمر التالي: python manage.py makemigrations ثم تطبيق عمليات التهجير باستخدام الأمر التالي: python manage.py migrate عمليات CRUD حان الوقت الآن لنتعمق في التطبيق نفسه، فمن غير المحتمل أن ننشئ جميع المتحكمات Controllers أولًا ثم نصمم القوالب ثم نتقل إلى الموجّهات Routers عند إنشاء تطبيقات واقعية، بل يجب أن نفكر من وجهة نظر المستخدم وفي الإجراءات التي قد يرغب في اتخاذها. يجب أن يمتلك المستخدم القدرة على إجراء أربع عمليات لكل مورد، والذي هو Post في حالتنا، وهذه العمليات هي: الإنشاء Create لإدراج بيانات جديدة في قاعدة البيانات القراءة Read لاسترداد البيانات من قاعدة البيانات التحديث Update لتعديل البيانات الموجودة مسبقًا في قاعدة البيانات الحذف Delete لإزالة البيانات من قاعدة البيانات ويشار إلى هذه العمليات مع بعضها البعض باسم عمليات CRUD. إجراء الإنشاء Create لنبدأ أولًا بعملية الإنشاء، فلا تزال قاعدة البيانات فارغة، لذا يجب على المستخدم إنشاء منشور جديد، ولكنك تحتاج إلى موجّه إرسال عناوين URL لتوجيه نمط عنوان URL الذي هو ‎/post/create/‎ إلى دالة العرض View Function وهي post_create()‎ لإكمال هذا الإجراء. تحتاج دالة العرض post_create()‎ إلى التمييز بين نوع الطلب الوارد إلى السيرفر لذا يجب أن تحتوي على عنصر تحكم في التدفق كتعليمة if لتميز فيما إذا كان تابع الطلب هو GET، عندها ستعيد دالة العرض قالبًا يحتوي على استمارة HTML، لتسمح للمستخدم بتمرير المعلومات إلى الواجهة الخلفية، أما إرسال الاستمارة Form فيجب أن يكون ضمن طلب POST. لذا إذا كان تابع الطلب هو POST، فيجب إنشاء مورد Post جديد وحفظه. إليك مراجعةً مختصرة لتوابع HTTP في حال احتياجك إلى تجديد بعض المعلومات: تابع GET هو تابع طلبات HTTP الأكثر استخدامًا، ويُستخدم لطلب البيانات والموارد من الخادم يُستخدم تابع POST لإرسال البيانات إلى الخادم، ويستعمل عادة لإنشاء أو تحديث المورد يعمل تابع HEAD مثل تابع GET تمامًا، باستثناء أن استجابة HTTP تحتوي على الترويسة فقط دون الجسم، ويستخدم المطورون هذا التابع لأغراض تنقيح الأخطاء Debugging تابع PUT مشابه لتابع POST، مع اختلاف واحد بسيط، فإذا أرسلت مورد باستخدام التابع POST وكان المورد موجودًا مسبقًا على الخادم، فلن يسبب هذا الإجراء أي فرق على الخادم، أما التابع PUT فسيكرر تحديث هذا المورد بالبيانات المرسلة في كل مرة تجري فيها الطلب. يزيل تابع DELETE موردًا من الخادم. لنبدأ بموجّه إرسال عناوين URL، لذا انتقل إلى الملف djangoBlog/urls.py وضع فيه ما يلي: from django.urls import path from blog import views urlpatterns = [ path("post/create/", views.post_create, name="create"), ] ستحتاج بعد ذلك إلى دالة العرض post_create()‎، لذا انتقل إلى الملف blog/views.py وضع فيه الشيفرة البرمجية التالية: from django.shortcuts import redirect, render from .models import Post def post_create(request): if request.method == "GET": return render(request, "post/create.html") elif request.method == "POST": post = Post(title=request.POST["title"], content=request.POST["content"]) post.save() return redirect("home") تفحص الدالة post_create()‎ أولًا تابع طلب HTTP، فإذا كان تابع GET، فيجب إعادة القالب create.html، وإذا كان POST فيجب استخدام المعلومات التي يمرّرها طلب POST لإنشاء نسخة POST جديدة، ثم إعادة التوجيه إلى الصفحة الرئيسية التي سننشئها في الخطوة التالية. سننشئ القالب create.html، ولكن يجب إنشاء القالب templates/layout.html أولًا كما يلي: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <script src="https://cdn.tailwindcss.com"></script> {% block title %}{% endblock %} </head> <body class="container mx-auto font-serif"> <div class="bg-white text-black font-serif"> <div id="nav"> <nav class="flex flex-row justify-between h-16 items-center shadow-md"> <div class="px-5 text-2xl"> <a href="/"> My Blog </a> </div> <div class="hidden lg:flex content-between space-x-10 px-10 text-lg"> <a href="{% url 'create' %}" class="hover:underline hover:underline-offset-1">New Post</a> <a href="https://github.com/ericnanhu" class="hover:underline hover:underline-offset-1">GitHub</a> </div> </nav> </div> {% block content %}{% endblock %} <footer class="bg-gray-700 text-white"> <div class="flex justify-center items-center sm:justify-between flex-wrap lg:max-w-screen-2xl mx-auto px-4 sm:px-8 py-10"> <p class="font-serif text-center mb-3 sm:mb-0">Copyright © <a href="https://www.ericsdevblog.com/" class="hover:underline">Eric Hu</a></p> <div class="flex justify-center space-x-4"> . . . </div> </div> </footer> </div> </body> </html> لاحظ ‎{% url 'create' %}‎ في السطر 22، فهذه هي الطريقة التي يمكنك بها عكس عناوين URL اعتمادًا على أسمائها، حيث يتطابق الاسم create مع الاسم الذي أعطيته لموجّه الإرسال ‎post/create/‎. أضفنا أيضًا إطار عمل TailwindCSS عبر شبكة CDN في السطر 8 لجعل هذه الصفحة تبدو أفضل، ولكن يجب ألّا تفعل ذلك في بيئة الإنتاج. لننشئ بعد ذلك القالب templates/post/create.html، إذ يتوجب علينا إنشاء المجلد post له لتوضيح أن هذا القالب مخصص لإنشاء منشور: {% extends 'layout.html' %} {% block title %} <title>Create</title> {% endblock %} {% block content %} <div class="w-96 mx-auto my-8"> <h2 class="text-2xl font-semibold underline mb-4">Create new post</h2> <form action="{% url 'create' %}" method="POST"> {% csrf_token %} <label for="title">Title:</label><br> <input type="text" id="title" name="title" class="p-2 w-full bg-gray-50 rounded border border-gray-300 focus:ring-3 focus:ring-blue-300"><br> <br> <label for="content">Content:</label><br> <textarea type="text" id="content" name="content" rows="15" class="p-2 w-full bg-gray-50 rounded border border-gray-300 focus:ring-3 focus:ring-blue-300"></textarea><br> <br> <button type="submit" class="font-sans text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-md text-sm w-full px-5 py-2.5 text-center">Submit</button> </form> </div> {% endblock %} يحدّد السطر 10 الإجراء الذي ستتخذه هذه الاستمارة عند إرسالها، وتابع الطلب الذي ستستخدمه، ويضيف السطر 11 حماية من هجمات CSRF إلى الاستمارة لأغراض أمنية. لاحظ أيضًا الحقل <input> في السطرين 13 و 14، فالسمة Attribute التي هي name في هذا الحقل مهمة جدًا، حيث سيُربَط إدخال المستخدم بهذه السمة عند إرسال الاستمارة، ويمكنك بعد ذلك استرداد هذا الإدخال في دالة العرض كما يلي: title=request.POST["title"] وينطبق الأمر نفسه على العنصر <textarea> في السطرين 17 و 18، ويجب أن يكون الزر من النوع type="submit"‎ ليعمل بنجاح. إجراء القائمة List لننشئ الآن صفحة رئيسية لعرض قائمة بجميع المنشورات، حيث سنبدأ بعنوان URL لهذه الصفحة في الملف djangoBlog/urls.py كما يلي: path("", views.post_list, name="home"), ثم ننتقل إلى دالة العرض في الملف blog/views.py ونضع فيه ما يلي: def post_list(request): posts = Post.objects.all() return render(request, "post/list.html", {"posts": posts}) وسيتضمن القالب list.html المحتويات التالية: {% extends 'layout.html' %} {% block title %} <title>My Blog</title> {% endblock %} {% block content %} <div class="max-w-screen-lg mx-auto my-8"> {% for post in posts %} <h2 class="text-2xl font-semibold underline mb-2"><a href="{% url 'show' post.pk %}">{{ post.title }}</a></h2> <p class="mb-4">{{ post.content | truncatewords:50 }}</p> {% endfor %} </div> {% endblock %} تتكرر التعليمة ‎{% for post in posts %}‎ على جميع المنشورات posts، ويُسنَد كل عنصر إلى المتغير post، وتمرّر التعليمة ‎{% url 'show' post.pk %}‎ المفتاح الرئيسي Primary Key للمنشور post إلى موجّه إرسال عنوان URL للصفحة show التي سننشئها لاحقًا. تستخدم التعليمة ‎{{ post.content | truncatewords:50 }}‎ المرشّح truncatewords لاقتطاع المحتوى بحيث يحتوي على أول 50 كلمة. إجراء العرض Show يجب أن يعرض إجراء العرض محتوى منشور معين، مما يعني أن عنوان URL الخاص به يجب أن يحتوي على شيء فريد يسمح لجانغو بتحديد نسخة واحدة من Post فقط، ويكون هذا الشيء الفريد هو المفتاح الرئيسي Primary Key، لذا ضع ما يلي في الملف djangoBlog/urls.py: path("post/<int:id>", views.post_show, name="show"), سيُسنَد العدد الصحيح الذي يلي ‎post/‎ إلى المتغير id، ويُمرّر إلى دالة العرض في الملف blog/views.py كما يلي: def post_show(request, id): post = Post.objects.get(pk=id) return render(request, "post/show.html", {"post": post}) وسيتضمن القالب المقابل templates/post/show.html المحتويات التالية: {% extends 'layout.html' %} {% block title %} <title>{{ post.title }}</title> {% endblock %} {% block content %} <div class="max-w-screen-lg mx-auto my-8"> <h2 class="text-2xl font-semibold underline mb-2">{{ post.title }}</h2> <p class="mb-4">{{ post.content }}</p> <a href="{% url 'update' post.pk %}" class="font-sans text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-md text-sm w-full px-5 py-2.5 text-center">Update</a> </div> {% endblock %} إجراء التحديث Update ضع ما يلي في الملف djangoBlog/urls.py لتحديد موجّه إرسال عنوان URL لإجراء التحديث: path("post/update/<int:id>", views.post_update, name="update"), وتكون دالة العرض كما يلي في الملف blog/views.py: def post_update(request, id): if request.method == "GET": post = Post.objects.get(pk=id) return render(request, "post/update.html", {"post": post}) elif request.method == "POST": post = Post.objects.update_or_create( pk=id, defaults={ "title": request.POST["title"], "content": request.POST["content"], }, ) return redirect("home") ملاحظة: أضيف التابع update_or_create()‎ حديثًا إلى الإصدار 4.1 من جانغو. وسيتضمن القالب المقابل templates/post/update.html المحتويات التالية: {% extends 'layout.html' %} {% block title %} <title>Update</title> {% endblock %} {% block content %} <div class="w-96 mx-auto my-8"> <h2 class="text-2xl font-semibold underline mb-4">Update post</h2> <form action="{% url 'update' post.pk %}" method="POST"> {% csrf_token %} <label for="title">Title:</label><br> <input type="text" id="title" name="title" value="{{ post.title }}" class="p-2 w-full bg-gray-50 rounded border border-gray-300 focus:ring-3 focus:ring-blue-300"><br> <br> <label for="content">Content:</label><br> <textarea type="text" id="content" name="content" rows="15" class="p-2 w-full bg-gray-50 rounded border border-gray-300 focus:ring-3 focus:ring-blue-300">{{ post.content }}</textarea><br> <br> <div class="grid grid-cols-2 gap-x-2"> <button type="submit" class="font-sans text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-md text-sm w-full px-5 py-2.5 text-center">Submit</button> <a href="{% url 'delete' post.pk %}" class="font-sans text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-md text-sm w-full px-5 py-2.5 text-center">Delete</a> </div> </form> </div> {% endblock %} إجراء الحذف Delete ضع ما يلي في الملف djangoBlog/urls.py لتحديد موجّه إرسال عنوان URL لإجراء الحذف: path("post/delete/<int:id>", views.post_delete, name="delete"), وتكون دالة العرض كما يلي في الملف blog/views.py: def post_delete(request, id): post = Post.objects.get(pk=id) post.delete() return redirect("home") لا يتطلب هذا الإجراء قالبًا، لأنه يعيد توجيهك إلى الصفحة الرئيسية بعد اكتمال الإجراء. بدء تشغيل الخادم لنبدأ الآن بتشغيل خادم التطوير ونرى النتيجة كما يلي: python manage.py runserver ستكون الصفحة الرئيسية للمدونة كما يلي: وتكون صفحة إنشاء منشور جديد كما يلي: وتكون صفحة عرض المنشور كما يلي: وتكون صفحة تحديث المنشور كما يلي: الخلاصة بهذا نكون قد أنهينا العمل على تطبيق مدونتنا البسيطة باستخدام Django وأنشأنا نموذج لتمثيل المنشورات في قاعدة البيانات، وتعلمنا كيف ننفيذ عمليات عبر توابع HTTP مثل GET و POST و PUT و DELETE. كما شرحنا خطوات تصميم واجهة المستخدم باستخدام القوالب لعرض المنشورات وإنشاء منشورات جديدة وتحديثها وحذفها. تابع المقال التالي من السلسلة للتعرف على خطوات إكمال المدونة. ترجمة -وبتصرّف- للمقال Django for Beginners #3 - The CRUD Operations لصاحبه Eric Hu. اقرأ أيضًا المقال السابق: استخدام بنية MTV لإنشاء مدونة بسيطة في جانغو إعداد بيئة تطوير تطبيقات جانغو Django بناء تطبيق مهام باستخدام جانغو Django وريآكت React رفع مستوى أمان تطبيقات جانغو في بيئة الإنتاج
  10. نشرح في مقال اليوم بنية نموذج-قالب-عرض Model-Template-View أو MTV اختصارًا التي يعتمد عليها إطار عمل جانغو Django لتطوير الويب حيث يكون النموذج Model مسؤولًا عن التفاعل مع قاعدة البيانات، ويجب أن يقابل كل نموذج جدولًا منها، ويعبر القالب Template عن جزء الواجهة الأمامية من التطبيق، وهو الشيء الذي سيراه المستخدم، أما العرض View فهو يتضمن المنطق البرمجي لواجهة التطبيق الخلفية، ومسؤولً عن استرداد البيانات من قاعدة البيانات عبر النماذج ووضعها في العرض المقابل وإعادة القالب المعروض إلى المستخدم في النهاية. هذه المقالة جزء من سلسلة من المقالات تشرح جانغو للمبتدئين على النحو التالي: الجزء الأول: البدء في إنشاء مدونة بسيطة الجزء الثاني: استخدام بنية MTV لإنشاء مدونة بسيطة الجزء الثالث: استخدام عمليات CRUD لإدارة المدونة الجزء الرابع: تطبيق المدونة الكامل الجزء الخامس: إضافة بعض الميزات المتقدمة إلى تطبيق المدونة مفهوم النماذج Models النموذج من أفضل ميزات جانغو Django، ففي أطر عمل الويب الأخرى ستحتاج إلى إنشاء نموذج وملف تهجير Migration، وملف التهجير هو مخطط Schema لقاعدة البيانات، يصف بنية قاعدة البيانات كأسماء الأعمدة وأنواعها، ويوفر النموذج واجهةً تتعامل مع معالجة البيانات بناءً على هذا المخطط، ولكنك ستحتاج إلى نموذج فقط في جانغو، ويمكن توليد ملفات التهجير المقابلة باستخدام أمر بسيط هو python manage.py makemigrations، مما يوفر عليك كثيرًا من الوقت. يحتوي كل تطبيق جانغو على ملف models.py واحد، ويجب تعريف جميع النماذج المرتبطة بالتطبيق بداخله، ويقابل كل نموذج ملف تهجير، والذي يقابل بدوره جدولًا في قاعدة البيانات. يمكنك التمييز بين الجداول الخاصة بالتطبيقات المختلفة من خلال إسناد بادئة لكل جدول تلقائيًا، حيث سيكون لجدول قاعدة البيانات المقابل لتطبيق blog الخاص بنا البادئة blog_‎. يوضح المثال التالي نموذجًا في الملف blog/models.py: from django.db import models class Person(models.Model): first_name = models.CharField(max_length=30) last_name = models.CharField(max_length=30) يمكنك بعد ذلك توليد ملف التهجير باستخدام الأمر التالي: python manage.py makemigrations ويجب أن يبدو ملف التهجير الناتج blog/migrations/0001_initial.py كما يلي: # Generated by Django 4.1.2 on 2022-10-19 23:13 from django.db import migrations, models class Migration(migrations.Migration): initial = True dependencies = [] operations = [ migrations.CreateModel( name="Person", fields=[ ( "id", models.BigAutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("first_name", models.CharField(max_length=30)), ("last_name", models.CharField(max_length=30)), ], ), ] يمكنك اعتبار ملف التهجير بمثابة مخطط يوضح كيف يجب أن يبدو جدول قاعدة البيانات، ويمكنك استخدام الأمر التالي لتطبيق هذا المخطط: python manage.py migrate يجب أن تبدو قاعدة بياناتك db.sqlite3 كما يلي: لا تحاول تعديل أو حذف ملفات التهجير إذا كنت مبتدئًا، ودع جانغو يطبّق كل شيء نيابةً عنك إلّا إن كنت تعرف ما تفعله تمامًا. يمكن لجانغو في معظم الحالات اكتشاف التغييرات التي أجريتها في النماذج حتى إذا حذفتَ شيئًا وتوليد ملفات الترحيل وفقًا لذلك. سينشئ النموذج في مثالنا جدول قاعدة بيانات person، وسيحتوي هذا الجدول على ثلاثة أعمدة اسمها id و first_name و last_name، حيث يُنشأ العمود id تلقائيًا كما هو موضح في ملف التهجير، ويُستخدم هذا العمود كمفتاح رئيسي Primary Key للفهرسة Indexing افتراضيًا. يسمَّىCharField()‎ بنوع الحقل ويعرِّف نوع العمود، ويسمَّى max_length بخيار الحقل ويحدّد معلومات إضافية حول هذا العمود. أهم نواع حقول النموذج وخياراتها يوضّح الجدول التالي بعض أنواع الحقول الأكثر استخدامًا وننصحك بالاطلاع على جميع أنواع الحقول وخياراتها من توثيق جانغو الرسمي. نوع الحقل وصف عنه BigAutoField ينشئ عمودًا نوعه عدد صحيح يتزايد تلقائيًا، يُستخدم عادةً مع العمود id. BooleanField ينشئ عمودًا قيمه منطقية True أو False. DateField و DateTimeField يُستخدم لإضافة تواريخ وأوقات كما يوحي اسمه. FileField و ImageField ينشئ عمودًا لتخزين المسار الذي يؤشّر إلى الملف أو الصورة المرفوعة. IntegerField و BigIntegerField تتراوح قيم الأعداد الصحيحة Integer من ‎-2147483648 إلى 2147483647، وتتراوح قيم الأعداد الصحيحة الكبيرة Big Integer من ‎-9223372036854775808 إلى 9223372036854775807 SlugField الاسم المختصر Slug هو نسخة بسيطة من عنوان URL للاسم أو العنوان. CharField و TextField ينشئ كل من CharField و TextField عمودًا لتخزين السلاسل النصية Strings، ولكن يقابل TextField مربع نص أكبر في صفحة مدير جانغو Django Admin التي سنتحدث عنها لاحقًا في هذه السلسلة من المقالات. يوضح الجدول التالي بعض خيارات الحقول الأكثر استخدامًا: خيار الحقل وصف عنه blank يسمح للحقل بأن يحتوي على إدخال فارغ. choices يمنح الحقل خيارات متعددة، حيث سنوضّح ذلك لاحقًا عندما نصل إلى مدير جانغو. default يعطي الحقل قيمةً افتراضية. unique يتأكد من أن كلّ عنصر في العمود فريد، ويُستخدَم عادةً لتحديد الاسم المختصر Slug والحقول الأخرى التي يُفترَض أن تحتوي على قيم فريدة. خيارات الصنف Meta يمكنك أيضًا إضافة الصنف Class الذي هو Meta في صنف النموذج، والذي يحتوي على معلومات إضافية حول هذا النموذج مثل اسم جدول قاعدة البيانات وخيارات الترتيب والأسماء المفردة وجمعها التي يمكن أن يقرأها الإنسان كما يلي: class Category(models.Model): priority = models.IntegerField() class Meta: ordering = ["priority"] verbose_name_plural = "categories" توابع النموذج توابع النموذج هي دوال معرَّفة في صنف النموذج، تسمح بتطبيق إجراءات مخصصة على النسخة الحالية من كائن النموذج كما في المثال التالي: class Person(models.Model): first_name = models.CharField(max_length=50) last_name = models.CharField(max_length=50) birth_date = models.DateField() def baby_boomer_status(self): "Returns the person's baby-boomer status." import datetime if self.birth_date < datetime.date(1945, 8, 1): return "Pre-boomer" elif self.birth_date < datetime.date(1965, 1, 1): return "Baby boomer" else: return "Post-boomer" في الكود السابق، يفحص جانغو تاريخ ميلاد الشخص ويعيد حالة تمثل إن كانت فترة ولادته قبل زيادة المواليد التي حدثت بعد الحرب العالمية الثانية Pre-boomer، أم خلالها Baby-Boomer أم بعدها Post-boomer للشخص عند استدعاء التابع baby_boomer_status()‎. ملاحظة: الكائنات والتوابع والخاصيات مفاهيم مهمة جدًا في لغات البرمجة، لذا ننصح بمطالعة مقال كائنات وأصناف بايثون لمزيد من المعلومات. وراثة النماذج ستحتاج إلى أكثر من نموذج واحد في معظم تطبيقات الويب، وسيكون لبعضها حقول مشتركة، لذا يمكنك إنشاء نموذج أب Parent Model يحتوي على الحقول المشتركة، ثم تجعل النماذج الأخرى ترث هذا النموذج الأب كما يلي: class CommonInfo(models.Model): name = models.CharField(max_length=100) age = models.PositiveIntegerField() class Meta: abstract = True class Student(CommonInfo): home_group = models.CharField(max_length=5) لاحظ أن النموذج CommonInfo نموذج مجرد Abstract، مما يعني أنه لا يقابل نموذجًا فرديًا فعليًا، بل يستخدم كأب لنماذج أخرى. لنولّد الآن ملف تهجير جديد blog/migrations/0002_student.py باستخدام الأمر التالي للتحقق من ذلك: python manage.py makemigrations سيتولّد ملف التهجير التالي: # Generated by Django 4.1.2 on 2022-10-19 23:28 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("blog", "0001_initial"), ] operations = [ migrations.CreateModel( name="Student", fields=[ ( "id", models.BigAutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("name", models.CharField(max_length=100)), ("age", models.PositiveIntegerField()), ("home_group", models.CharField(max_length=5)), ], options={ "abstract": False, }, ), ] لاحظ إنشاء الجدول Student فقط. علاقات قاعدة البيانات تحدثنا عن كيفية إنشاء جداول فردية، ولكن لا تكون هذه الجداول مستقلة تمامًا في معظم التطبيقات، بل توجد علاقات بينها، فمثلًا قد يكون لديك فئة Category تنتمي إليها منشورات متعددة، ويكون كل منشور في المدونة تابع لمستخدم معين وما إلى ذلك، لذا توجد ثلاثة أنواع أساسية للتعبير عن العلاقات بين جداول قاعدة البيانات وهي: علاقة واحد إلى واحد One-to-one وعلاقة متعدد إلى واحد Many-to-one وعلاقة متعدد إلى متعدد Many-to-many وسنشرح تاليًا المزيد حول هذه العلاقات وطريقة وصفها ضمن نماذج جانغو. علاقة واحد إلى واحد علاقة واحد إلى واحد هي العلاقة الأسهل، فمثلًا يمكن أن يكون لكل شخص هاتف واحد، ويمكن أن يعود كل هاتف إلى شخص واحد، حيث يمكننا وصف هذه العلاقة في النماذج كما يلي: class Person(models.Model): name = models.CharField(max_length=100) class Phone(models.Model): person = models.OneToOneField('Person', on_delete=models.CASCADE) يعمل نوع الحقل OneToOneField مثل أي نوع حقل آخر، ولكنه يتطلب وسيطين على الأقل هما: الوسيط الأول هو اسم النموذج الآخر الذي يرتبط به هذا النموذج بعلاقة، والوسيط الثاني هو on_delete الذي يعرّف الإجراء الذي سيتخذه جانغو عند حذف البيانات، ويتعلق هذا المعامل بلغة SQL أكثر من جانغو، لذا لن نتحدث عنه بالتفصيل، ولكن إن كنت مهتمًا، فاطلع على بعض القيم المتاحة للمعامل on_delete. يمكنك الآن إنشاء عمليات تهجير وتطبيقها لهذه النماذج ومعرفة ما يحدث، وإذا واجهتك مشاكل أثناء تشغيل الأوامر التالية، فاحذف الملف db.sqlite3 وملفات التهجير للبدء من جديد: python manage.py makemigrations python manage.py migrate لاحظ أن نوع الحقل OneToOneField أنشأ العمود person_id في الجدول blog_phone، وسيخزّن هذا العمود معرّف id الشخص الذي يمتلك هذا الهاتف. علاقة متعدد إلى واحد يمكن أن تحتوي كل فئة على منشورات متعددة وينتمي كل منشور إلى فئة واحدة مثلًا، حيث يشار إلى هذه العلاقة باسم علاقة متعدد إلى واحد التي نعرّفها كما يلي: class Category(models.Model): name = models.CharField(max_length=100) class Post(models.Model): category = models.ForeignKey('Category', on_delete=models.CASCADE) ينشئ نوع الحقل ForeignKey العمود category_id في الجدول blog_post، والذي يخزن معرّف id الفئة التي ينتمي إليها هذا المنشور. علاقة متعدد إلى متعدد تكون علاقة متعدد إلى متعدد أكثر تعقيدًا بعض الشيء، فمثلًا يمكن أن يكون لكل مقال وسوم Tags متعددة، ويمكن أن يكون لكل وسم مقالات متعددة: class Tag(models.Model): name = models.CharField(max_length=100) class Post(models.Model): tags = models.ManyToManyField('Tag') ستنشئ الشيفرة البرمجية السابقة جدولًا جديدًا بالاسم post_tags بدلًا من إنشاء عمود جديد، وسيحتوي هذا الجدول الجديد على عمودين هما post_id و tag_id، مما يتيح لك تحديد جميع الوسوم المرتبطة بمنشور معين والعكس صحيح. لنفترض أن لدينا الجدول التالي مثلًا لتوضيح الأمور: post_id tag_id 1 1 2 1 3 2 1 2 2 3 يمتلك المنشور الذي له المعرّف id=1 وسمين tags لهما المعرّف id=1 والمعرّف id=2. إذا أردنا عكس الأمور للعثور على المنشورات باستخدام الوسم، فيمكننا أن نرى منشورين لهما المعرّف id=3 والمعرّف id=1 بالنسبة للوسم ذي المعرّف id=2. طبقة العرض View طبقة العرض مكون مهم في تطبيق جانغو ، فهي المكان الذي نكتب فيه المنطق البرمجي للواجهة الخلفية. يسترد العرض البيانات من قاعدة البيانات من خلال النموذج المقابل ويعالج هذه البيانات المستردة ويضعها في الموقع المقابل في القالب ويعرض هذا القالب ويعيده إلى المستخدم. لا تعمل دالة العرض View Function على استرداد البيانات فقط، إذ توجد أربع عمليات أساسية يمكن تطبيقها على البيانات في معظم تطبيقات الويب، وهي الإنشاء Create والقراءة Read والتحديث Update والحذف Delete، ويشار إلى هذه العمليات مجتمعة بالاسم CRUD، والتي سنوضّحها في مقال لاحق. تحدّثنا عن النماذج في القسم السابق، ولكننا ما زلنا لا نعرف كيفية استرداد البيانات أو تخزينها باستخدام النموذج، حيث يقدم جانغو واجهة برمجة تطبيقات API بسيطة لمساعدتنا في ذلك، تسمى QuerySet. لنفترض أن لدينا النموذج blog/models.py التالي: class Category(models.Model): name = models.CharField(max_length=100) class Tag(models.Model): name = models.CharField(max_length=200) class Post(models.Model): title = models.CharField(max_length=255) content = models.TextField() pub_date = models.DateField() category = models.ForeignKey(Category, on_delete=models.CASCADE) tags = models.ManyToManyField(Tag) يمكنك معالجة البيانات بمساعدة QuerySet من خلال هذا النموذج ضمن دوال العرض التي توجد في الملف blog/views.py. إنشاء البيانات وحفظها لنفترض أنك تريد إنشاء فئة جديدة كما يلي: # استيراد نموذج الفئة‫ Category from blog.models import Category # إنشاء نسخة جديدة من النموذج‫ Category category = Category(name="New Category") # حفظ الفئة التي أنشأناها في قاعدة البيانات category.save() يجب أن يكون ما سبق سهلًا إذا كنت على دراية بمفهوم البرمجة كائنية التوجه، حيث أنشأنا في المثال السابق نسخة جديدة من الكائن Category واستخدمنا التابع save()‎ الذي ينتمي إلى هذا الكائن لحفظ المعلومات في قاعدة البيانات. توجد علاقة متعدد إلى واحد بين الفئة والمنشور، وقد عرفناها باستخدام الحقل ForeignKey()‎، مع افتراض أن لدينا عدد كافي من السجلات في قاعدة البيانات. from blog.models import Category, Post # ‫Post.objects.get(pk=1)‎ هي الطريقة التي نسترجع بها المنشور الذي له المفتاح الرئيسي pk=1، # حيث يرمز‫ pk إلى المفتاح الرئيسي Primary Key الذي يمثّل المعرّف id عادةً إن لم نحدّد خلاف ذلك. post = Post.objects.get(pk=1) # استرداد الفئة التي اسمها‫ "New Category" new_category = Category.objects.get(name="New Category") # إسناد المتغير‫ new_category إلى حقل فئة المنشور وحفظه post.category = new_category post.save() توجد أيضًا علاقة متعدد إلى متعدد بين المنشورات والوسوم كما يلي: from blog.models import Tag, Post post1 = Post.objects.get(pk=1) # استرداد المنشور 1 tag1 = Tag.objects.get(pk=1) # استرداد الوسم 1 tag2 = Tag.objects.get(pk=2) # استرداد الوسم 2 tag3 = Tag.objects.get(pk=3) # استرداد الوسم 3 tag4 = Tag.objects.get(pk=4) # استرداد الوسم 4 tag5 = Tag.objects.get(pk=5) # استرداد الوسم 5 post.tags.add(tag1, tag2, tag3, tag4, tag5) # إضافة الوسوم من 1 إلى 5 إلى المنشور 1 استرداد البيانات استرداد الكائنات أكثر تعقيدًا قليلًا مما رأيناه سابقًا، لذا سنوضّح فيما يلي كيف سنجد سجلًا معينًا في قاعدة بياناتنا التي تحتوي على آلاف السجلات إن لم نكن نعرف المعرّف id، وسنوضّح كيفية الحصول على مجموعة من السجلات التي تناسب معايير معينة بدلًا من الحصول على سجل واحد مثلًا. توابع QuerySet تتيح توابع QuerySet استرداد البيانات بناءً على معايير معينة، ويمكن الوصول إليها باستخدام السمة Attribute التي هي objects، حيث يُستخدَم التابع get()‎ الذي رأيناه سابقًا لاسترداد سجل معين كما يلي: first_tag = Tag.objects.get(pk=1) new_category = Category.objects.get(name="New Category") ويمكن أيضًا استرداد جميع السجلات باستخدام التابع all()‎: Post.objects.all() يعيد التابع all()‎ مجموعة من السجلات والتي نسميها مجموعة الاستعلام QuerySet، ويمكنك تحسين هذه المجموعة من خلال سَلسَلة التابع filter()‎ أو التابع exclude()‎ مع التابع all()‎ كما يلي: Post.objects.all().filter(pub_date__year=2024) سيؤدي ذلك إلى إعادة جميع المنشورات المنشورة في عام 2024، ويُسمَّى pub_date__year بوسيط البحث في الحقول Field Lookup، وسنناقش هذا الموضوع بالتفصيل لاحقًا. يمكننا أيضًا استبعاد المنشورات المنشورة عام 2024 كما يلي: Post.objects.all().exclude(pub_date__year=2024) هناك أيضًا العديد من توابع QuerySet الأخرى بالإضافة إلى get()‎ و all()‎ و filter()‎ و exclude()‎، ولكن لن نتحدث عنها جميعًا، لذا اطّلع على القائمة الكاملة لجميع توابع QuerySet من توثيق جانغو الرسمي. وسطاء عمليات البحث في الحقول Field Lookups وسطاء عمليات البحث في الحقول هي وسطاء الكلمات المفتاحية للتوابع get()‎ و filter()‎ و exclude()‎، والتي تعمل بطريقة مشابهة لتعليمة WHERE في لغة SQL، وتأخذ الصيغة fieldname__lookuptype=value مع وجود شرطة سفلية مزدوجة. Post.objects.all().filter(pub_date__lte='2024-01-01') pub_date هو اسم الحقل و lte هو نوع البحث الذي يرمز إلى أقل من أو يساوي، وستُعاد جميع المنشورات التي يكون فيها تاريخ النشر pub_date أقل من أو يساوي ‎2024-01-01‎. يمكن أيضًا استخدام وسطاء عمليات البحث في الحقول للعثور على السجلات التي لها علاقة بالسجل الحالي كما في المثال التالي، وستُعاد جميع المنشورات التي تنتمي إلى الفئة التي اسمها "Django": Post.objects.filter(category__name='Django') يمكننا تطبيق ذلك عكسيًا مثل إعادة جميع الفئات التي تحتوي على منشورٍ واحد على الأقل الذي يحتوي عنوانه على الكلمة "Django" كما يلي: Category.objects.filter(post__title__contains='Django') يمكننا أيضًا المرور عبر علاقات متعددة كما يلي: Category.objects.filter(post__author__name='Admin') يعيد الاستعلام هنا جميع الفئات التي تحتوي على منشورات ينشرها المستخدم Admin، إذ يمكن وضع سلسلة من العلاقات بالعدد الذي نريده. ويمكنك مطالعة جميع وسطاء عمليات البحث في الحقول التي يمكنك استخدامها من توثيق جانغو الرسمي. حذف الكائنات نستخدم التابع delete()‎ لحذف سجلٍ ما، حيث ستحذف الشيفرة البرمجية التالية المنشور الذي له المفتاح الرئيسي pk=1: post = Post.objects.get(pk=1) post.delete() ويمكننا أيضًا استخدامه لحذف سجلات متعددة معًا كما يلي: Post.objects.filter(pub_date__year=2022).delete() سيؤدي ذلك إلى حذف جميع المنشورات في عام 2022، ولكن قد يتعلّق السجل الذي نحذفه بسجل آخر مثل محاولة حذف فئة تحتوي على منشورات متعددة كما يلي: category = Category.objects.get(pk=1) category.delete() يحاكي جانغو سلوك قيد SQL التالي ON DELETE CASCADE، مما يعني حذف جميع المنشورات التي تنتمي إلى هذه الفئة أيضًا، ولكن إذا أردتَ تغيير ذلك، فيمكنك تغيير خيار on_delete إلى شيء آخر، لذا اطّلع على جميع خيارات on_delete المتاحة في توثيق جانغو الرسمي. دالة العرض View Function وضّحنا ما يمكن فعله داخل دالة العرض، وسنوضّح كيف تبدو دالة العرض الكاملة كما في المثال التالي، حيث تُعرَّف جميع العروض ضمن الملف views.py: from django.shortcuts import render from blog.models import Post # أنشئ عروضك الخاصة هنا def my_view(request): posts = Post.objects.all() return render(request, 'blog/index.html', { 'posts': posts, }) هناك شيئان يجب الانتباه إليهما في هذا المثال، أولهما أن دالة العرض تأخذ المتغير request كدخل، وهذا المتغير هو كائن HttpRequest يُمرَّر تلقائيًا إلى العرض من موجّه إرسال Dispatcher عناوين URL. اطّلع على مقال مدخل إلى HTTP لمزيد من المعلومات حول التواصل بين عميل وخادم التطبيق. يحتوي request على الكثير من المعلومات حول طلب HTTP الحالي، فمثلًا يمكننا الوصول إلى تابع طلب HTTP وكتابة شيفرات برمجية مختلفة لتوابع مختلفة كما يلي: if request.method == 'GET': do_something() elif request.method == 'POST': do_something_else() ملاحظة: اطّلع على جميع المعلومات التي يمكنك الوصول إليها من كائن request في توثيق جانغو الرسمي. والشيء الآخر الذي يجب الانتباه إليه هو استيراد اختصار Shortcut اسمه render()‎، ثم استخدامه لتمرير المتغير posts إلى القالب blog/index.html. يُطلق عليه اختصار لأنه من المفترض تحميل القالب افتراضيًا باستخدام التابع loader()‎ وعرض هذا القالب مع البيانات المُسترَدة وإعادة كائن HttpResponse، ولكن بسّط جانغو هذه العملية باستخدام الاختصار render()‎. لن نتحدث عن الطريقة المعقدة لذلك، لأننا لن نستخدمها في هذا المقال ويمكنك مطالعة جميع دوال الاختصار Shortcut Functions في توثيق جانغو الرسمي. نظام قوالب جانغو طبقة القوالب Template هي جزء الواجهة الأمامية لتطبيق جانغو، لذا تكون ملفات القالب هي شيفرات مكتوبة بلغة HTML لأنها تمثّل ما تراه في المتصفح، ولكن قد تكون الأمور أكثر تعقيدًا قليلًا، فإن احتوى القالب على شيفرات HTML فقط، فسيكون موقع الويب ساكنًا بالكامل، ولا نريد ذلك، لذا يجب أن يخبر القالبُ دالةَ العرض بمكان وضع البيانات المسترَدة. عمليات الضبط Configurations يجب أولًا تغيير شيء ما في الملف settings.py، إذ يجب أن تخبر جانغو بمكان وضع ملفات القوالب، لذا لننشئ المجلد templates، حيث اخترنا وضعه ضمن المجلد الجذر للمشروع، ولكن يمكنك نقله لمكان آخر. . ├── blog ├── db.sqlite3 ├── djangoBlog ├── env ├── manage.py ├── mediafiles ├── staticfiles └── templates انتقل إلى الملف settings.py وابحث عن TEMPLATES. TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [ 'templates', ], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] لنعدّل الخيار DIRS الذي يؤشر إلى المجلد templates، ولنتحقق الآن من عمل هذا الإعداد، لذا ننشئ نمط عنوان URL جديد يؤشّر إلى العرض test()‎ في الملف djangoBlog/urls.py كما يلي: from django.urls import path from blog import views urlpatterns = [ path('test/', views.test), ] ولننشئ الآن العرض test()‎ في الملف blog/views.py كما يلي: def test(request): return render(request, 'test.html') انتقل إلى المجلد templates وأنشئ القالب test.html كما يلي: <!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Test Page</title> </head> <body> <p>This is a test page.</p> </body> </html> شغّل خادم التطوير وانتقل إلى العنوان http://127.0.0.1:8000/‎، وستظهر الصفحة التالية: لغة قوالب جانغو لنناقش الآن محرّك قوالب جانغو بالتفصيل، وتذكّر أنه يمكننا إرسال البيانات من العرض إلى القالب كما يلي: def test(request): return render(request, 'test.html', { 'name': 'Jack' }) تُسنَد السلسلة النصية 'Jack' إلى المتغير name وتمرَّر إلى القالب، ويمكننا عرض المتغير name ضمن القالب باستخدام أقواس معقوصة مزدوجة {{ }} كما يلي: <p>Hello, {{ name }}</p> حدّث المتصفح، وسترى النتيجة التالية: ولكن لا تكون البيانات المُمرَّرة إلى القالب سلسلة نصية بسيطة في أغلب الحالات كما في المثال التالي: def test(request): post = Post.objects.get(pk=1) return render(request, 'test.html', { 'post': post }) المتغير post في المثال السابق هو قاموس Dictionary، حيث يمكنك الوصول إلى العناصر الموجودة في هذا القاموس في القالب كما يلي: {{ post.title }} {{ post.content }} {{ post.pub_date }} المرشحات Filters تحوّل المرشحات قيم المتغيرات، فمثلًا إذا كان لدينا المتغير django الذي قيمته 'the web framework for perfectionists with deadlines' ووضعنا المرشح title مع هذا المتغير كما يلي: {{ django|title }} فسيُحوَّل القالب إلى ما يلي: The Web Framework For Perfectionists With Deadlines اطّلع على جميع المرشحات المُضمَّنة في جانغو من توثيق جانغو الرسمي. الوسوم Tags تضيف الوسوم ميزات لغات البرمجة مثل التحكم في التدفق والحلقات إلى شيفرة HTML، مما يوفر الكثير من الوقت والموارد، إذ لن نضطر إلى كتابة الشيفرة البرمجية نفسها مرارًا وتكرارًا. تُعرَّف جميع الوسوم باستخدام {% %} مثل حلقة for التالية: <ul> {% for athlete in athlete_list %} <li>{{ athlete.name }}</li> {% endfor %} </ul> وتكون تعليمة if كما يلي: {% if somevar == "x" %} This appears if variable somevar equals the string "x" {% endif %} وتكون تعليمة if-else كما في المثال التالي: {% if athlete_list %} Number of athletes: {{ athlete_list|length }} {% elif athlete_in_locker_room_list %} Athletes should be out of the locker room soon! {% else %} No athletes. {% endif %} اطّلع على جميع الوسوم المُضمَّنة في جانغو من توثيق جانغو الرسمي، حيث توجد الكثير من المرشحات والوسوم المفيدة الأخرى في نظام قوالب جانغو، والتي سنتحدث عنها لاحقًا. نظام الوراثة Inheritance الفائدة الأساسية لاستخدام قوالب جانغو هي أنك لست بحاجة إلى كتابة الشيفرة البرمجية نفسها مرارًا وتكرارًا، فمثلًا يوجد شريط تنقل Navigation Bar وتذييل Footer في جميع صفحات تطبيق الويب النموذجي، ومن الصعب تكرار هذه الشيفرة البرمجية في كل صفحة من صيانة هذا التطبيق، لذا يقدم جانغو طريقة سهلة للغاية لحل هذه المشكلة كما سنوضح فيما يلي. لننشئ الملف layout.html في المجلد templates، ويمثّل هذا الملف المكان الذي نعرّف فيه تخطيط القالب الخاص بنا كما يلي، ولكننا لم نضع الشيفرة البرمجية الخاصة بالتذييل وشريط التنقل لتسهيل القراءة: <!DOCTYPE html> <html> <head> {% block meta %} {% endblock %} ‏<-- ‫استورد شيفرة CSS هنا –!> </head> <body> <div class="container"> <!-- ضع شيفرة شريط التنقل هنا --> {% block content %} {% endblock %} <!-- ضع شيفرة تذييل الصفحة هنا --> </div> </body> </html> لاحظ أننا عرّفنا كتلتين في هذا الملف هما meta و content باستخدام الوسم ‎{% block ... %}‎، وعرّفنا القالب home.html التالي لاستخدام هذا التخطيط: {% extends 'layout.html' %} {% block meta %} <title>Page Title</title> <meta charset="UTF-8"> <meta name="description" content="Free Web tutorials"> <meta name="keywords" content="HTML, CSS, JavaScript"> <meta name="author" content="John Doe"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> {% endblock %} {% block content %} <p>This is the content section.</p> {% include 'vendor/sidebar.html' %} {% endblock %} سيجد جانغو أولًا الملف layout.html عند استدعاء القالب السابق، ويملأ الكتلتين meta و content بالمعلومات الموجودة في صفحة home.html. لاحظ وجود شيء آخر في هذا القالب، حيث تخبر التعليمة ‎{% include 'vendor/sidebar.html' %}‎ جانغو بالبحث عن القالب templates/vendor/sidebar.html ووضعه في الصفحة، حيث يتضمن القالب sidebar.html المحتويات التالية مثلًا: <p>This is the sidebar.</p> لا يُعَد ذلك شريطًا جانبيًا، ولكن يمكننا استخدامه لتوضيح عمل نظام الوراثة، وتأكد أيضًا من صحة العرض كما يلي: from django.shortcuts import render def home(request): return render(request, 'home.html') وتأكّد من أن موجّه إرسال عنوان URL الخاص بك يؤشّر إلى هذا العرض كما يلي: path('home/', views.home), افتح متصفحك وانتقل إلى العنوان http://127.0.0.1:8000/home، ويجب أن ترى الصفحة التالية: ترجمة -وبتصرّف- للمقال Django for Beginners #2 - The MTV Structure لصاحبه Eric Hu. اقرأ أيضًا المقال السابق: جانغو للمبتدئين الجزء الأول: البدء في إنشاء مدونة بسيطة إنشاء تطبيق حديث باستخدام Django و Vue | الجزء الأول: الأساسيات العروض والقوالب في Django - الجزء الأول العروض والقوالب في Django - الجزء الثاني مدخل إلى إطار عمل الويب جانغو Django
  11. يمكن ضرب المصفوفة بمصفوفة عمودية إذا كانت أبعادهما تسمح بذلك، حيث يُعَد ذلك عمليةً أساسيةً في الرسوميات الحاسوبية وفي العديد من المجالات الأخرى؛ إذ تُجرَى هذه العملية ملايين المرات في الثانية عند تشغيل برنامج الرسوميات ثلاثية الأبعاد. سنوضح في هذا المقال المواضيع التالية: أبعاد معامَلات ونتيجة ضرب المصفوفات بمصفوفة عمودية. أبعاد معامَلات ونتيجة ضرب مصفوفة سطرية بالمصفوفات. ضرب المصفوفات بمصفوفة عمودية. ضرب مصفوفة سطرية بالمصفوفات. استخدام الجداء النقطي لضرب المصفوفات. الوصول إلى نتيجة ضرب المصفوفات من خلال تطبيق عمليات متعددة من الجداء النقطي. قلب Flipping المصفوفة العمودية عند ضرب المصفوفات بمصفوفة عمودية. قلب أعمدة المصفوفة عند ضرب مصفوفة سطرية بها. التعريفات الرياضية لعمليات ضرب المصفوفات وخطوات تنفيذها. الخاصية غير التبديلية لضرب المصفوفات. رأينا مسبقًا عمليات شبيهة بالضرب، والتي هي: ضرب عدد حقيقي (مقدار سلمي) Scalar بعدد حقيقي، والذي يعطي عددًا حقيقيًا. ضرب مصفوفة عمودية بعدد حقيقي، والذي يعطي مصفوفة عمودية. الجداء النقطي Dot Product للمصفوفات العمودية، والذي يعطي عددًا حقيقيًا. ضرب المصفوفات بالمصفوفة العمودية ناتج ضرب مصفوفةٍ ما بمصفوفة عمودية هو مصفوفة عمودية كما في المثال التالي: سنشرح لاحقًا تفاصيل كيفية تطبيق هذه العملية، ولكن لاحظ الآن أبعاد معامَلات العملية ونتيجتها؛ إذ يمكن تشكيل عملية ضرب مصفوفة بمصفوفة عمودية إذا كان عدد أعمدة المصفوفة مساويًا لعدد أسطر المصفوفة العمودية كما يلي: Matrix(R x N) x Col. Matrix(N x 1) = Resulting Col. Matrix(R x 1)‎‎ يمكن توضيح ذلك في المثال التالي: Matrix(2‎ x 2) x Col. Matrix(2‎ x 1) = Resulting Col. Matrix(2‎ x 1)‎ نعرض الأبعاد بالشكل: "Row x Column" (بحيث يكون Row هو عدد الصفوف وColumn هو عدد الأعمدة)، إذ تكون المصفوفة التي أبعادها R x 1 مصفوفةً عمودية، لأنها تحتوي على عمود واحد. لاحظ أيضًا كيف تكون أبعاد نتيجة ضرب مصفوفة بمصفوفة عمودية في المثال التالي: Matrix(5 x 5) x Col. Matrix(5 x 1) = Resulting Col. Matrix(5 x 1)‎ لنحدّد الآن ما إذا كان من الممكن ضرب مصفوفةٍ بمصفوفة عمودية وما هي أبعاد نتيجة عملية الضرب في الأمثلة الموجودة في الجدول التالي: أبعاد المصفوفة أبعاد المصفوفة العمودية أبعاد النتيجة الأبعاد 3‎ x 3 الأبعاد 3‎ x 1 الأبعاد 3‎ x 1 الأبعاد 3‎ x 2 الأبعاد 2‎ x 1 الأبعاد 3‎ x 1 الأبعاد 2‎ x 3 الأبعاد 2‎ x 1 غير ممكنة الأبعاد 4‎ x 3 الأبعاد 3‎ x 1 الأبعاد 4‎ x 1 الأبعاد 3‎ x 5 الأبعاد 5‎ x 1 الأبعاد 3‎ x 1 لاحظ أنه من غير الممكن تشكيل عملية الضرب التي تبدو كما يلي: ‎Matrix(2 x 1) x Col. Matrix(2 x 2) = Resulting Col. Matrix‎‎(غير ممكنة) وضع المصفوفة العمودية على الجهة اليمنى فقط من عملية الضرب إذا ضربنا مصفوفةً مستطيلةً بمصفوفة عمودية، فستكون المصفوفة العمودية دائمًا على يمين المصفوفة المستطيلة؛ إذ لا يمكن ضربهما إّلا إذا كان "البعد الداخلي Inner Dimension" (البعد M مثلًا) لكل مصفوفة متساويًا: Matrix(N x M) x Col. Matrix(M x 1) = Resulting Col. Matrix(N x 1)‎ حيث يمكن تشكيل عملية الضرب التي تبدو كما يلي: ‎Matrix(1 x 2) x Col. Matrix(2 x 1) = عدد حقيقي‎‎(1 x 1) استخدام الجداء النقطي لضرب المصفوفات يبدو استخدام الجداء النقطي لضرب المصفوفات ممكنًا من خلال النظر إلى الأبعاد المصفوفات، ولكنه ليس دليلًا على إمكانية ذلك. إليك المثال التالي: قد يبدو غريبًا بعض الشيء عَدُّ العدد الحقيقي (المقدار السلمي Scalar) كائنًا أبعاده "1 × 1"، ولكن تبقي هذه الطريقة الأمور متناسقةً فيما بينها. لاحظ أن هذا المثال يشبه الجداء النقطي (وهو كذلك فعلًا)، وبالتالي ستتشكّل عملية الضرب كما يلي: إذًا لنوجد الآن ناتج عملية الضرب التالية كما يلي: قلب المصفوفة العمودية من المفيد التفكير في "قلب" المصفوفة العمودية عند تشكيل الجداء النقطي ليحاذي كل صف من المصفوفة، ثم ضرب وجمع العناصر المتقابلة كما في المثال التالي: وفي المثال التالي أيضًا: لنشكّل الآن عملية الضرب التالية: تدريب عملي اُحسب الجداء النقطي في الجدول التالي. ستجد ناتج الجداء في عمود النتيجة: المعامل الأول المعامل الثاني النتيجة المصفوفة المصفوفة العمودية ‎‎ الناتج ‎-2 المصفوفة المصفوفة العمودية ‎‎ الناتج ‎-2 المصفوفة المصفوفة العمودية ‎‎ الناتج ‎2 سنستخدم عمليات الجداء النقطي السابقة لإيجاد نتيجة ضرب المصفوفة بالمصفوفة العمودية كما يلي: إذًا يمكننا الحصول على ناتج عملية ضرب مصفوفةٍ بمصفوفة عمودية من خلال تطبيق الجداء النقطي للمصفوفة العمودية مع كل صف من المصفوفة الأخرى، ولكن يجب أن تسمح أبعاد كل معامَل بذلك؛ إذ يساعد ذلك على قلب المصفوفة العمودية، بحيث يمكن محاذاة عناصرها مع عناصر المصفوفة كما يلي: اقلب المصفوفة العمودية واحسب النتيجة ما يلي: إليك أيضًا بعض عمليات الضرب الأخرى التي قد ترغب في التدرب عليها، ولكن تذكّر أن تقلب المصفوفة العمودية وتضعها فوق المصفوفة، ثم طبّق الجداء النقطي للمصفوفة العمودية التي قلبناها مع صف واحد من المصفوفة في كل مرة: العملية النتيجة الناتج ‎‎ الناتج الناتج ‎ استخدام عمليات متعددة من الجداء نقطي لإجراء عملية ضرب المصفوفة السطرية بالمصفوفات جرّب الآن إجراء العملية التالية: لاحظ أن أبعاد المصفوفات متوافقة، لذا يمكن إجراء العملية كما يلي: يمكنك أن ترى من المثال السابقة أنه يمكن إجراء عملية ضرب مصفوفةٍ سطرية بمصفوفةٍ ما من خلال تطبيق عدة عمليات جداء نقطية، ولكن يجب قلب أعمدة المصفوفة في هذه الحالة كما يلي: وضع المصفوفة السطرية على الجهة اليسرى من عملية الضرب لا يمكن إجراء النوع التالي من عمليات الضرب، لأن الأبعاد غير صحيحة: Matrix (3 x 4) x Row Matrix (1 x 3)‎ إذًا يجب أن تكون المصفوفة السطرية على الجهة اليسرى عند ضرب مصفوفة سطرية بمصفوفة مستطيلة، حيث تكون أبعاد المعامَلات والنتيجة كما يلي: Row Matrix (1 x M) x Matrix (M x C) = Result Row Matrix (M x C)‎ ملاحظة: المصفوفة التي أبعادها 1xC هي مصفوفة سطرية مكوَّنة من عددٍ "C" من الأعمدة. نوضّح في الأمثلة الموجودة في الجدول التالي ما إذا كانت عملية الضرب ممكنة وما هي أبعاد نتيجة العملية: المعامل الأول المعامل الثاني النتيجة 3‎ x 3 ‫ 3‎ x 1 ‫ 3‎ x 1 1‎ x 2 ‫ 2‎ x 2 ‫ 1‎ x 2 1‎ x 2 ‫ 2‎ x 3 ‫ 1‎ x 3 4‎ x 4 ‫ 3‎ x 1 ‫ غير ممكنة 1‎ x 5 ‫ 5‎ x 3 ‫ 1‎ x 3 لنحاول الآن إجراء عملية الضرب التالية: وينتج ما يلي: التعريفات الدقيقة لعمليات الضرب قد تشعر بعدم الارتياح عند تعريف العمليات الرياضية بهذه الطريقة غير الرسمية، لذا إليك بعض التعريفات الأدق: ضرب مصفوفة بمصفوفة عمودية Matrix times Column Matrix: ليكن لدينا المصفوفة A التي أبعادها M x C، والمصفوفة العمودية x التي أبعادها C x 1، ويكون ناتج الجداء Ax مصفوفة عمودية أبعادها M x 1؛ إذ يتشكّل عنصرها i من ناتج الجداء النقطي للصف i من المصفوفة A مع المصفوفة العمودية x. ضرب مصفوفة سطرية بمصفوفة: ليكن لدينا المصفوفة السطرية x التي أبعادها 1‎ x R والمصفوفة **A التي أبعادها R x M، ويكون ناتج الجداء *xA* مصفوفة سطرية أبعادها 1‎ x M، حيث يتشكّل عنصرها الأول من ناتج الجداء النقطي للمصفوفة السطرية x مع العمود i من المصفوفة A. لنحاول الآن إجراء عملية الضرب التالية: وسينتج لدينا ما يلي: خطوات إجراء عملية ضرب المصفوفات يتضمن الضرب دائمًا عمليات جداء نقطية للصفوف الموجودة على اليسار والأعمدة الموجودة على اليمين وفقًا للتعريفات السابقة، لذا يجب اتباع الخطوات التالية لإجراء أيٍّ من هذه الحسابات: دراسة أبعاد معاملات عملية الضرب حيث: يجب أن تكون الأبعاد الداخلية متساوية: 1xN و NxC. نتجاهل الأبعاد الداخلية للعثور على أبعاد النتيجة: 1xC. نكتب مصفوفة فارغة بالأبعاد الصحيحة لتمثّل المصفوفة الناتجة. ملء جميع عناصر المصفوفة الناتجة كما يلي: نقلب كل عمود موجود على اليمين إلى صف على اليسار. ينتج كل عنصر من عناصر المصفوفة الناتجة عن عملية جداء نقطي. تدريب عملي لا تُعَد Ax و xA متساويتين؛ فإذا كان تطبيق عملية الضرب Ax ممكنة، فستكون أبعاد xA غير متوافقة عند ذلك، ويمكن في بعض الأحيان تشكيل كلٍّ من هاتين العمليتين، ولكن النتائج ستكون مختلفة، وبالتالي يمكن القول أن ضرب المصفوفات ليست عمليةً تبديلية. إذًا لنختبر مهاراتك في الأمثلة التالية: العملية النتيجة الناتج الناتج الناتج وصلنا إلى نهاية هذا المقال الذي تعرّفنا من خلاله على ضرب مصفوفة بمصفوفة عمودية وضرب مصفوفة سطرية بمصفوفةٍ ما، وسنتعرّف في المقال التالي على كيفية ضرب مصفوفة مستطيلة بمصفوفة أخرى. ترجمة -وبتصرُّف- للفصل Matrix-Column Matrix Multiplicaton من كتاب Vector Math for 3D Computer Graphics لصاحبه Bradley Kjell. اقرأ أيضًا المقال السابق: تعرف على المصفوفات Matrices وعملياتها البسيطة في التصاميم 3D الجداء الشعاعي في التصاميم ثلاثية الأبعاد وخاصياته وحسابه كيفية جمع وطرح المصفوفات العمودية والمصفوفات السطرية الأشعة والنقاط والمصفوفات العمودية في الرسوميات الحاسوبية ثلاثية الأبعاد
  12. جانغو Django هو إطار عمل ويب عالي المستوى مجاني ومفتوح المصدر ومكتوب بلغة البرمجة بايثون Python، ويُستخدَم على نطاق واسع لبناء تطبيقات ويب معقدة، وهو معروف بقدرته على التعامل مع حركة المرور العالية وميزات الأمان الخاصة به ومكوناته القابلة لإعادة الاستخدام. يتبع جانغو نمط معمارية نموذج-عرض-متحكم Model-View-Controller أو MVC اختصارًا ويأتي مع واجهة مُدمَجة لإدارة الموقع مع دعم الاستيثاق Authentication وربط الكائنات بالعلاقات Object-Relational Mapping أو ORM اختصارًا الذي يسمح بتعريف مخطط قاعدة بياناتك باستخدام أصناف Classes بايثون. يتمتع جانغو بمجتمع كبير ونشط يوفر مجموعة كبيرة من البرامج التعليمية والحزم والموارد الأخرى التي يمكن للمطورين استخدامها، إذ تُبنَى العديد من المواقع الإلكترونية المعروفة وذات حركة المرور العالية -مثل إنستغرام Instagram وبنترست Pinterest وموقع واشنطن تايمز The Washington Times- باستخدام إطار عمل جانغو. يُعَد جانغو أحد أكثر أطر عمل الويب شعبيةً وقوة للغة بايثون، وتستخدمه كلٌّ من الشركات الناشئة والكبيرة على نطاق واسع، مما يجعله خيارًا رائعًا لبناء تطبيقات ويب قوية وقابلة للتوسّع، وتؤدي مرونته وقابليته للتوسع إلى جعله الخيار الأفضل للمطورين والمؤسسات التي تريد إنشاء تطبيقات ويب معقدة وصغيرة الحجم. هذه المقالة جزء من سلسلة من المقالات تشرح جانغو للمبتدئين على النحو التالي: البدء في إنشاء مدونة بسيطة استخدام بنية MTV لإنشاء مدونة بسيطة استخدام عمليات CRUD لإدارة المدونة تطبيق المدونة الكامل إضافة بعض الميزات المتقدمة للمدونة تثبيت الأدوات اللازمة توجد بعض الأدوات التي يجب تثبيتها على جهازك قبل البدء، حيث تحتاج مبدئيًا لغة برمجة (بايثون)، وقاعدة بيانات (SQLite)، وخادم (سنستخدم خادم التطوير المُدمَج مع جانغو) لتشغيل التطبيق بنجاح. تذكّر أن هذه الأدوات مُخصَّصة لبيئة التطوير Dev Environment المخصصة لإنشاء واختبار التطبيق فقط، لذا لا يجب استخدام هذه الأدوات في بيئة الإنتاج Production Environment. تحتاج أيضًا إلى بيئة تطوير متكاملة IDE مثل بيئة باي تشارم PyCharm أو محرّر شيفرات برمجية على الأقل، حيث سنستخدم في هذا المقال المحرّر VS Code لأنه برنامج مجاني. اتبّع الآن الخطوات التالية للحصول على هذه الأدوات: تنزيل بايثون. تنزيل SQLite. تنزيل VS Code. تنزيل PyCharm. إنشاء مشروع جانغو جديد حان الوقت لبدء كتابة الشيفرة البرمجية بعد تثبيت الأدوات السابقة، لذا أنشئ أولًا مجلد عمل جديد باستخدام الأمر التالي لتنظيم كل شيء: mkdir django-demo ثم انتقل إلى هذا المجلد باستخدام الأمر التالي: cd django-demo أنشئ بعد ذلك بيئة افتراضية جديدة للغة بايثون ضمن مجلد العمل باستخدام الأمر التالي، حيث ستكون هذه البيئة الافتراضية معزولة عن بيئة بايثون في نظامك ومشاريع بايثون الأخرى التي قد تكون على جهازك. python -m venv env إذا استخدمتَ نظام لينكس Linux أو ماك macOS، فقد تضطر إلى تشغيل أمر python3 بدلًا من python، ولكننا سنستخدم python للتبسيط. سيعمل الأمر السابق على إنشاء المجلد env الذي يحتوي على البيئة الافتراضية التي أنشأتها، ويمكنك تنشيط هذه البيئة الافتراضية باستخدام الأمر التالي: source env/bin/activate وإذا استخدمتَ نظام ويندوز Windows، فاستخدم الأمر التالي بدلًا من ذلك: env/Scripts/activate إذا نشطت البيئة الافتراضية بنجاح، فسيكون موجّه سطر الأوامر الخاص بك كما يلي: (env) eric@djangoDemo:~/django-demo$ ملاحظة: يعني (env) هنا أنك تعمل حاليًا في بيئة افتراضية اسمها env. حان الوقت الآن لتهيئة مشروع جانغو جديد، حيث تحتاج إلى تثبيت حزمة Django من خلال تشغيل الأمر التالي: python -m pip install Django ثم يمكنك استخدام أمر django-admin لإنشاء مشروع جانغو جديد كما يلي: django-admin startproject djangoBlog وسينشَأ المجلد الجديد djangoBlog التالي: يُفضَّل إعادة هيكلة المشروع بعض الشيء بحيث نبدأ من المجلد الجذر للمشروع كما يلي، ولكن لا حاجة لذلك إن لم ترغب في ذلك. إنشاء تطبيق المدونة لا يزال المشروع فارغًا حاليًا، ولاحظ أن بنية هذا المشروع بسيطة مقارنةً ببنية مشروع لارافيل Laravel، وسنناقش كل ملف في مجلد المشروع بالتفصيل لاحقًا. يتيح جانغو إنشاء تطبيقات متعددة في مشروع واحد، فمثلًا يمكن أن يكون هناك تطبيق blog وتطبيق gallery وتطبيق forum ضمن مشروع واحد. يمكن أن تتشارك هذه التطبيقات في نفس الملفات الثابتة (ملفات CSS وجافاسكربت JavaScript) والصور ومقاطع الفيديو أو يمكن أن تكون مستقلة تمامًا عن بعضها البعض، إذ يعتمد ذلك على احتياجاتك الخاصة. سننشئ في هذا المقال تطبيق مدونة blog فقط، لذا نفّذ الأمر التالي في الطرفية: python manage.py startapp blog يجب أن ترى مجلد blog جديد ضمن مجلد جذر المشروع، ويمكنك سرد محتوى المجلد باستخدام الأمر التالي: ls وإذا أردتَ تضمين الملفات المخفية أيضًا في النتيجة، فاستخدم الأمر التالي: ls -a وإذا أردتَ رؤية بنية الملفات فاستخدم الأمر التالي: tree ولكن قد تحتاج إلى تثبيت برنامج tree ليعمل الأمر السابق بنجاح اعتمادًا على نظام تشغيلك. سنستخدم الأمر السابق لعرض بنية الملفات، حيث يجب أن يكون لمشروعك البنية التالية حاليًا: . ├── blog │ ├── admin.py │ ├── apps.py │ ├── __init__.py │ ├── migrations │ ├── models.py │ ├── tests.py │ └── views.py ├── djangoBlog │ ├── asgi.py │ ├── __init__.py │ ├── __pycache__ │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── env │ ├── bin │ ├── include │ ├── lib │ ├── lib64 -> lib │ └── pyvenv.cfg └── manage.py يجب بعد ذلك تسجيل تطبيق blog الجديد في جانغو، لذا انتقل إلى الملف settings.py وابحث عن القائمة INSTALLED_APPS: INSTALLED_APPS = [ 'blog', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', ] بدء تشغيل الخادم يمكنك الآن بدء تشغيل خادم التطوير لاختبار نجاح عمل كل شيء، لذا افتح الطرفية وشغّل الأمر التالي: python manage.py runserver وسيظهر الخرج التالي في واجهة سطر الأوامر: Watching for file changes with StatReloader Performing system checks... System check identified no issues (0 silenced). You have 18 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions. Run 'python manage.py migrate' to apply them. October 19, 2022 - 01:39:33 Django version 4.1.2, using settings 'djangoBlog.settings' Starting development server at http://127.0.0.1:8000/ Quit the server with CONTROL-C. افتح المتصفح وانتقل إلى العنوان http://127.0.0.1:8000/‎، وستظهر الصفحة التالية: بنية التطبيق لنتحدث عن البنية التالية لتطبيق جانغو الجديد وما يفعله كل ملف قبل أن نبدأ في كتابة الشيفرة البرمجية: . ├── blog │ ├── admin.py │ ├── apps.py │ ├── __init__.py │ ├── migrations │ ├── models.py │ ├── tests.py │ └── views.py ├── djangoBlog │ ├── asgi.py │ ├── __init__.py │ ├── __pycache__ │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── env │ ├── bin │ ├── include │ ├── lib │ ├── lib64 -> lib │ └── pyvenv.cfg └── manage.py المجلد الجذر يحتوي المجلد الجذر على الملفات والمجلدات التالية: manage.py: يمثل أداة سطر الأوامر التي تتيح لك التفاعل مع مشروع جانغو بطرق مختلفة. djangoBlog: مجلد المشروع الرئيسي الذي يحتوي على ملف الإعدادات ونقطة الدخول إلى المشروع. blog: يمثل تطبيق المدونة blog. مجلد المشروع يحتوي هذا المجلد على الملفات التالية: djangoBlog/__init__.py: ملف فارغ يخبر بايثون بأن هذا المجلد يجب عَدّه حزمة بايثون. djangoBlog/settings.py: ملف الإعدادات أو الضبط Configuration لمشروع جانغو. djangoBlog/urls.py: يحتوي على التصريحات عن عناوين URL لمشروع جانغو، ويمثل جدول محتويات تطبيق جانغو الخاص بك، حيث سنتحدث عنه أكثر لاحقًا. djangoBlog/asgi.py: يُعَد نقطة الدخول لخوادم الويب المتوافقة مع واجهة ASGI لتخديم مشروعك. djangoBlog/wsgi.py: نقطة الدخول لخوادم الويب المتوافقة مع واجهة WSGI لتخديم مشروعك. مجلد التطبيق يحتوي هذا المجلد على المجلدات والملفات التالية: blog/migrations: يحتوي هذا المجلد على جميع ملفات التهجير Migration لتطبيق المدونة، حيث ينشئ جانغو هذه الملفات تلقائيًا بناءً على نماذجك Models على عكس إطار عمل لارافيل. blog/admin.py: يأتي جانغو أيضًا مع لوحة مُدمَجة لإدارة الموقع، حيث يحتوي هذا الملف على جميع عمليات الضبط الخاصة بهذه اللوحة. blog/models.py: تصف النماذج Models بنية وعلاقة قاعدة البيانات، وتُنشَأ ملفات التهجير بناءً على هذا الملف. blog/views.py: يكافئ المتحكمات Controllers في لارافيل، ويحتوي على المنطق البرمجي الأساسي لهذا التطبيق. ضبط Configuring مشروع جانغو توجد بعض التغييرات التي يجب إجراؤها على الملف settings.py قبل البدء بالشروع كما سنوضح فيما يلي. المضيفون المسموح بهم تُعد القائمة ALLOWED_HOSTS قائمة بالنطاقات Domains التي يُسمَح لموقع جانغو بتخديمها، وهي إجراء أمني لمنع هجمات ترويسات مضيف HTTP أو HTTP Host Header Attacks، والتي يمكن حدوثها حتى عند إجراء العديد من عمليات ضبط خادم الويب الآمنة ظاهريًا. لاحظ أنه ما زال بإمكاننا الوصول إلى موقعنا باستخدام المضيف 127.0.0.1 حتى وإن كانت القائمة ALLOWED_HOSTS فارغة حاليًا، والسبب هو التحقق من صحة المضيف مقابل ‎['.localhost', '127.0.0.1', '[::1]']‎ عندما تكون قيمة DEBUG هي True وتكون القائمة ALLOWED_HOSTS فارغة. قاعدة البيانات DATABASES هو قاموس يحتوي على إعدادات قاعدة البيانات التي يحتاج موقعنا إلى استخدامها، حيث يستخدم جانغو قاعدة بيانات SQLite افتراضيًا، وهي قاعدة بيانات خفيفة الوزن وتتكون من ملف واحد فقط. يجب أن تكون قاعدة بيانات SQLite كافيةً لمشروعنا التجريبي الصغير، لكنها لن تناسب المواقع الكبيرة، لذا إذا أردتَ استخدام قواعد بيانات أخرى، فإليك المثال التالي: DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", "NAME": "<database_name>", "USER": "<database_user_name>", "PASSWORD": "<database_user_password>", "HOST": "127.0.0.1", "PORT": "5432", } } ملاحظة: يوصي جانغو باستخدام قاعدة بيانات PostgreSQL في بيئة الإنتاج. توجد العديد من البرامج التعليمية التي تعلمك كيفية استخدام جانغو مع قاعدة بيانات MongoDB، ولكنها تُعَد فكرة سيئة، إذ قد تكون قاعدة بيانات MongoDB حلًا رائعًا، ولكنها لا تعمل بطريقة جيدة مع إطار عمل جانغو. الملفات الساكنة Static Files وملفات الوسائط Media Files يجب أن نهتم أيضًا بالملفات الساكنة وملفات الوسائط، فالملفات الساكنة هي ملفات CSS وملفات جافاسكربت، وملفات الوسائط هي الصور ومقاطع الفيديو والأمور الأخرى التي قد يرفعها المستخدم. الملفات الساكنة Static Files يجب أولًا تحديد مكان تخزين هذه الملفات، حيث سننشئ المجلد static ضمن تطبيق المدونة لموقع جانغو الخاص بنا، ويُعَد هذا المجلد هو المكان الذي نخزّن فيه الملفات الساكنة لتطبيق blog أثناء التطوير. blog ├── admin.py ├── apps.py ├── __init__.py ├── migrations ├── models.py ├── static ├── tests.py └── views.py تختلف الأمور بعض الشيء في بيئة الإنتاج، إذ يجب أن ننشئ مجلدًا مختلفًا ضمن المجلد الجذر للمشروع، ولنسمّيه بالاسم staticfiles. . ├── blog ├── db.sqlite3 ├── djangoBlog ├── env ├── manage.py └── staticfiles ثم يجب تحديد هذا المجلد في الملف settings.py كما يلي: STATIC_ROOT = "staticfiles/" نخبر بعد ذلك جانغو بعنوان URL الذي يجب استخدامه عند الوصول إلى هذه الملفات في المتصفح. ليس من الضروري أن يكون هذا العنوان هو ‎/static، ولكن تأكد من أنه لا يتداخل مع عمليات ضبط عناوين URL الخاصة بنا والتي سنتحدث عنها لاحقًا. STATIC_URL = "static/" ملفات الوسائط Media Files نضبط ملفات الوسائط باستخدام الطريقة نفسها، حيث يمكنك إنشاء المجلد mediafiles في المجلد الجذر للمشروع كما يلي: . ├── blog ├── db.sqlite3 ├── djangoBlog ├── env ├── manage.py ├── mediafiles └── staticfiles ثم نحدّد الموقع وعنوان URL في الملف settings.py كما يلي: # ملفات الوسائط MEDIA_ROOT = "mediafiles/" MEDIA_URL = "media/" موجه إرسال Dispatcher عناوين URL في جانغو يحتوي مجال تطوير الويب على بنية نموذج-عرض-متحكم Model-View-Controller (أو MVC اختصارًا)، حيث يكون النموذج Model في هذه البنية مسؤولًا عن التفاعل مع قاعدة بياناتنا، ويجب أن يقابل كلُّ نموذج جدولَ قاعدة بيانات واحد. العرض View هو جزء الواجهة الأمامية Frontend من التطبيق، وهو ما يمكن للمستخدمين رؤيته، والمتحكم Controller هو المنطق البرمجي للواجهة الخلفية للتطبيق مثل استرداد البيانات من قاعدة البيانات عبر النماذج ووضعها في العرض المقابل وإعادة القالب المعروض إلى المستخدم في النهاية. جانغو هو إطار عمل ويب مُصمَّم بناءً على بنية MVC مع مصطلحات مختلفة، فبنية جانغو هي بنية نموذج-قالب-عرض Model-Template-View (أو MTV اختصارًا)، فالقالب Template هو الواجهة الأمامية، والعرض هو المنطق البرمجي للواجهة الخلفية. سنركّز في هذا المقال على فهم هذه الطبقات، ولكن يجب أولًا البدء بنقطة الدخول لكل تطبيق ويب، والتي هي موجّه إرسال Dispatcher عناوين URL، حيث يقرأ هذا الموجّه عنوان URL ويوجّه المستخدم إلى الصفحة الصحيحة عندما يكتب المستخدم عنوان URL ويضغط على مفتاح Enter. عمليات ضبط عناوين URL الأساسية يُخزَّن ضبط عناوين URL في الملف example/urls.py كما يلي: djangoBlog ├── asgi.py ├── __init__.py ├── __pycache__ ├── settings.py ├── urls.py └── wsgi.py يوضّح المثال التالي ما يجب أن يبدو عليه موجّه إرسال عناوين URL: from django.contrib import admin from django.urls import path urlpatterns = [ path('admin/', admin.site.urls), ] يقرأ موجّه الإرسال المعلومات من عنوان URL ويعيد عرضًا، ولكن لا تخلط بين هذا العرض وعرض إطار عمل لارافيل، فالعرض في جانغو يمثّل متحكّمًا في لارافيل. إذا تبع النطاقُ admin/‎ في هذا المثال، فسيوجّه موجّه الإرسال المستخدم إلى صفحة المدير Admin. تمرير المعاملات باستخدام موجه إرسال عناوين URL في جانغو يكون من الضروري في بعض الأحيان أخذ مقاطع من عنوان URL وتمريرها إلى العرض بوصفها معاملات إضافية، فمثلًا إذا أردتَ عرض صفحة التدوينة، فستحتاج إلى تمرير المعاملَين id أو slug الخاصين بالتدوينة إلى العرض حتى تتمكّن من استخدام هذه المعلومات الإضافية للعثور على التدوينة التي تبحث عنها. إليك فيما يلي الطريقة التي يمكننا تمرير المعاملات باستخدامها: from django.urls import path from blog import views urlpatterns = [ path('post/<int:id>', views.post), ] ستلتقط الأقواس الزاويّة جزءًا من عنوان URL كمعامل، حيث يُسمَّى int على الجانب الأيسر بمحوِّل المسار Path Converter الذي يلتقط معاملًا من النوع الصحيح، ويمكن مطابقة أيّ سلسلة نصية باستثناء المحرف / عند عدم تضمين المحوّل. لاحظ وجود اسم المعامل على الجانب الأيمن، وهو ما سنحتاج إلى استخدامه في العرض. تتوفّر محوّلات المسارات التالية افتراضيًا: str: يطابق أيّ سلسلة نصية غير فارغة باستثناء فاصل المسار '/' افتراضيًا عند عدم تضمين محوّلٍ في التعبير. int: يطابق الصفر أو أيّ عدد صحيح موجب، ويعيد قيمة من النوع int. slug: يطابق أيّ سلسلة نصية للاسم المختصر Slug، والذي يتكون من حروف أو أرقام ASCII، بالإضافة إلى محارف الشرطة والشرطة السفلية مثل building-your-1st-django-site. uuid: يطابق معرّف UUID المنسَّق، حيث يجب تضمين شرطات، ويجب أن تكون الحروف صغيرة لمنع ربط عناوين URL متعددة مع الصفحة نفسها مثل المعرّف 075194d3-6885-417e-a8a8-6c931e272f00، ويعيد نسخةً من UUID. path: يطابق أي سلسلة نصية غير فارغة مع فاصل المسار '/'، مما يتيح لك المطابقة مع مسار URL كامل بدلًا من المطابقة مع مقطعٍ من مسار URL كما هو الحال مع المحوِّل str. استخدام التعابير النمطية Regular Expressions لمطابقة أنماط عناوين URL قد يكون النمط Pattern الذي تريد مطابقته أكثر تعقيدًا في بعض الحالات، وبالتالي يمكنك استخدام التعابير النمطية Regular Expressions لمطابقة أنماط عناوين URL. يجب استخدام التابع re_path()‎ بدلًا من التابع path()‎ لاستخدام التعابير النمطية: from django.urls import path, re_path from . import views urlpatterns = [ path('articles/2003/', views.special_case_2003), re_path(r'^articles/(?P<year>[0-9]{4})/$', views.year_archive), re_path(r'^articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/$', views.month_archive), re_path(r'^articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/(?P<slug>[\w-]+)/$', views.article_detail), ] استيراد عمليات ضبط عناوين URL الأخرى لنفترض أن لديك مشروع جانغو يحتوي على تطبيقات متعددة مختلفة، حيث إذا وضعت جميع عمليات ضبط عناوين URL في ملف واحد، فسيكون الأمر فوضويًا جدًا، ولكن يمكنك فصل عمليات ضبط عناوين URL ونقلها إلى تطبيقات مختلفة، فمثلًا يمكننا في مشروعنا إنشاء ملف urls.py جديد ضمن تطبيق blog كما يلي: from django.urls import path, include urlpatterns = [ path('blog/', include('blog.urls')) ] وهذا يعني أنه إذا احتوى عنوان URL على النمط http://www.mydomain.com/blog/xxxx، فسينتقل جانغو إلى الملف blog/urls.py ويطابق باقي عنوان URL. لنعرّف الآن أنماط عناوين URL لتطبيق blog باستخدام الطريقة نفسها كما يلي: from django.urls import path from blog import views urlpatterns = [ path('post/<int:id>', views.post), ] سيتطابق هذا النمط مع نمط عنوان URL الذي هو http://www.mydomain.com/blog/post/123. تسمية أنماط عناوين URL يمكنك تسمية نمط عنوان URL من خلال إعطائه معاملًا ثالثًا كما يلي: urlpatterns = [ path('articles/<int:year>/', views.year_archive, name='news-year-archive'), ] يؤدي ذلك إلى القدرة على عكس عناوين URL المطلقة من القالب، حيث سنتحدث عن ذلك لاحقًا عندما نصل إلى طبقة القوالب. تعرّفنا على أساسيات موجّه إرسال عناوين URL، ولكن لا زال هناك شيء لم نتطرق لشرحه وهو كيفية التمييز بين التوابع المختلفة لطلبات HTTP، حيث لا يمكنك في جانغو مطابقة توابع HTTP المختلفة ضمن موجه إرسال عناوين URL، إذ تتطابق جميع التوابع مع نمط عنوان URL نفسه، وبالتالي يجب استخدام التحكم في التدفق لكتابة شيفرات برمجية مختلفة لهذه التوابع في دوال العرض، حيث سنناقش ذلك بالتفصيل في المقالات اللاحقة من هذه السلسلة. ترجمة -وبتصرّف- للمقال Django for Beginners #1 - Getting Started لصاحبه Eric Hu. اقرأ أيضًا حزم بايثون الثمانية التي تسهل تعاملك مع Django مدخل إلى إطار عمل الويب جانغو Django تعرف على أمان تطبيقات جانغو بناء تطبيق مهام باستخدام جانغو Django وريآكت React
  13. سننشئ في هذا المقال الأخير من هذه السلسلة جزء الواجهة الأمامية من التطبيق، ولكن لنضع خطة أولًا، حيث سيكون لدينا الصفحة الرئيسية لتطبيق المدونة التي تعرض قائمة بجميع المنشورات الحديثة، وصفحة فئة Category المنشور التي تعرض قائمة المنشورات ضمن فئة معينة، وصفحة الوسم Tag التي تعرض قائمة المنشورات التي لها وسم مُحدَّد، وصفحة المنشور التي تعرض محتوى منشور معين، وصفحة بحث تعرض قائمة المنشورات بناءً على استعلام بحث معين. كما ستحتوي جميع هذه الصفحات على شريط جانبي مع مربع بحث وقائمة الفئات والوسوم، وستحتوي صفحة المنشور أيضًا على قسم المنشورات ذات الصلة في نهايتها. تحدّثنا عن قاعدة البيانات والنماذج Models في المقال السابق، وسنبدأ الآن بالوِجهات Routes. إنشاء الوِجهات ننشئ الوِجهات في لارافيل ضمن الملف routes/web.php كما يلي: . . . // الصفحة الرئيسية Route::get('/', [PostController::class, 'home'])->name('home'); // قائمة المنشورات ضمن هذه الفئة Route::get('/category/{category}', [CategoryController::class, 'category'])->name('category'); // قائمة المنشورات التي لها هذا الوسم Route::get('/tag/{tag}', [TagController::class, 'tag'])->name('tag'); // عرض منشور واحد Route::get('/post/{post}', [PostController::class, 'post'])->name('post'); // قائمة المنشورات بناء على استعلام البحث Route::post('/search', [PostController::class, 'search'])->name('search'); . . . الصفحة الرئيسية يكون لكل من هذه الوِجهات تابع تحكم مقابل، حيث سنبدأ أولًا بالمتحكم home()‎ إذًا لنضع ما يلي في الملف app/Http/Controllers/PostController.php: <?php namespace App\Http\Controllers; use App\Models\Category; use App\Models\Post; use App\Models\Tag; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Facades\Auth; use Illuminate\Database\Eloquent\Builder; class PostController extends Controller { /** * عرض الصفحة الرئيسية */ public function home(): View { $posts = Post::where('is_published', true)->paginate(env('PAGINATE_NUM')); $categories = Category::all(); $tags = Tag::all(); return view('home', [ 'posts' => $posts, 'categories' => $categories, 'tags' => $tags ]); } . . . } هناك شيئان يجب عليك ملاحظتهما في السطر 20 من الشيفرة البرمجية السابقة وهما: أولًا، تتأكد التعليمة where('is_published', true)‎ من استرداد المقالات المنشورة فقط. ثانيًا، يُعَد التابع paginate()‎ أحد توابع لارافيل Laravel المُضمَّنة، مما يسمح لك بإنشاء ترقيم للصفحات Pagination في تطبيقك بسهولة. يأخذ التابع paginate()‎ عددًا صحيحًا كدخل، فمثلًا يمثّل paginate(10)‎ عرض 10 عناصر في كل صفحة، وسيُستخدَم متغير الدخل في العديد من الصفحات، لذا يمكنك إنشاء متغير بيئة في ملف ‎.env كما يلي، ثم يمكنك استرداده في أيّ مكان باستخدام التابع env()‎: . . . PAGINATE_NUM=12 لننشئ الآن عرض View الصفحة الرئيسية المقابل. اطّلع على بنية القوالب التي أنشأئاها، وإليك بنية العروض التالية: resources/views ├── category.blade.php ├── home.blade.php ├── layout.blade.php ├── post.blade.php ├── search.blade.php ├── tag.blade.php ├── vendor │ ├── list.blade.php │ └── sidebar.blade.php └── welcome.blade.php سنبدأ أولًا بالعرض layout.blade.php الذي يمثل تخطيط الصفحة كما يلي: <!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> @vite(['resources/css/app.css', 'resources/js/app.js']) @yield('title') </head> <body class="container mx-auto font-serif"> <nav class="flex flex-row justify-between h-16 items-center border-b-2"> <div class="px-5 text-2xl"> <a href="/"> My Blog </a> </div> <div class="hidden lg:flex content-between space-x-10 px-10 text-lg"> <a href="https://github.com/ericnanhu" class="hover:underline hover:underline-offset-1" >GitHub</a > <a href="{{ route('dashboard') }}" class="hover:underline hover:underline-offset-1" >Dashboard</a > <a href="#" class="hover:underline hover:underline-offset-1">Link</a> </div> </nav> @yield('content') <footer class="bg-gray-700 text-white"> <div class="flex justify-center items-center sm:justify-between flex-wrap lg:max-w-screen-2xl mx-auto px-4 sm:px-8 py-10" > <p class="font-serif text-center mb-3 sm:mb-0"> Copyright © <a href="https://www.ericsdevblog.com/" class="hover:underline" >Eric Hu</a > </p> <div class="flex justify-center space-x-4">. . .</div> </div> </footer> </body> </html> يستخدم لارافيل الأداة Vite افتراضيًا لتجميع أصول المشروع كما في السطر 8، وثبّتنا الحزمة Breeze من لارافيل في المقال السابق، وتستخدم هذه الحزمة إطار عمل Tailwind CSS، حيث سيستورد السطر 8 من الشيفرة البرمجية السابقة تلقائيًا الملفين app.css و app.js المقابلَين. يمكنك استخدام إطار عمل مختلف أيضًا، ولكن يجب أن تعود إلى توثيقه للحصول على تفاصيل حول كيفية استخدامه مع لارافيل أو Vite. لننشئ الآن عرض الصفحة الرئيسية resources/views/home.blade.php كما يلي: @extends('layout') @section('title') <title>Home</title> @endsection @section('content') <div class="grid grid-cols-4 gap-4 py-10"> <div class="col-span-3 grid grid-cols-1">@include('vendor.list')</div> @include('vendor.sidebar') </div> @endsection ويكون عرض قائمة المنشورات resources/views/vendor/list.blade.php كما يلي: <!-- قائمة المنشورات --> <div class="grid grid-cols-3 gap-4"> @foreach ($posts as $post) <!-- المنشور --> <div class="mb-4 ring-1 ring-slate-200 rounded-md h-fit hover:shadow-md"> <a href="{{ route('post', ['post' => $post->id]) }}" ><img class="rounded-t-md object-cover h-60 w-full" src="{{ Storage::url($post->cover) }}" alt="..." /></a> <div class="m-4 grid gap-2"> <div class="text-sm text-gray-500"> {{ \Carbon\Carbon::parse($post->created_at)->format('M d, Y') }} </div> <h2 class="text-lg font-bold">{{ $post->title }}</h2> <p class="text-base"> {{ Str::limit(strip_tags($post->content), 150, '...') }} </p> <a class="bg-blue-500 hover:bg-blue-700 rounded-md p-2 text-white uppercase text-sm font-semibold font-sans w-fit focus:ring" href="{{ route('post', ['post' => $post->id]) }}" >Read more →</a > </div> </div> @endforeach </div> {{ $posts->links() }} يولّد السطر 9 Storage::url($post->cover)‎ عنوان URL الذي يؤشّر إلى صورة الغلاف. لا تُعَد الطريقة التي يخزن بها لارافيل العلامات الزمنية Timestamps سهلة الاستخدام، لذلك استخدمنا حزمة Carbon في السطر 14 لإعادة تنسيق العلامات الزمنية. استخدمنا أيضًا الدالة strip_tags()‎ في السطر 18 لإزالة وسوم HTML، ثم تحدِّد الدالة limit()‎ الحد الأقصى لطول السلسلة، وستوضَع نقاط ... مكان الجزء الزائد منها. استخدمنا التابع paginate()‎ لإنشاء مرقّم الصفحات في المتحكم، ولكن يمكننا عرض مرقّم الصفحات في العرض باستخدام التعليمة ‎{{ $posts->links() }}‎ الموجودة في السطر 30. لننشئ الآن عرض الشريط الجانبي resources/views/vendor/sidebar.blade.php كما يلي: <div class="col-span-1"> <div class="border rounded-md mb-4"> <div class="bg-slate-200 p-4">Search</div> <div class="p-4"> <form action="{{ route('search') }}" method="POST" class="grid grid-cols-4 gap-2" > {{ csrf_field() }} <input type="text" name="q" id="search" class="border rounded-md w-full focus:ring p-2 col-span-3" placeholder="Search something..." /> <button type="submit" class="bg-blue-500 hover:bg-blue-700 rounded-md p-2 text-white uppercase font-semibold font-sans w-full focus:ring col-span-1" > Search </button> </form> </div> </div> <div class="border rounded-md mb-4"> <div class="bg-slate-200 p-4">Categories</div> <div class="p-4"> <ul class="list-none list-inside"> @foreach ($categories as $category) <li> <a href="{{ route('category', ['category' => $category->id]) }}" class="text-blue-500 hover:underline" >{{ $category->name }}</a > </li> @endforeach </ul> </div> </div> <div class="border rounded-md mb-4"> <div class="bg-slate-200 p-4">Tags</div> <div class="p-4"> @foreach ($tags as $tag) <span class="mr-2" ><a href="{{ route('tag', ['tag' => $tag->id]) }}" class="text-blue-500 hover:underline" >{{ $tag->name }}</a ></span > @endforeach </div> </div> <div class="border rounded-md mb-4"> <div class="bg-slate-200 p-4">More Card</div> <div class="p-4"> <p> Lorem ipsum dolor sit amet consectetur adipisicing elit. Placeat, voluptatem ab tempore recusandae sequi libero sapiente autem! Sit hic reprehenderit pariatur autem totam, voluptates non officia accusantium rerum unde provident! </p> </div> </div> <div class="border rounded-md mb-4"> <div class="bg-slate-200 p-4">...</div> <div class="p-4"> <p> Lorem ipsum dolor sit amet consectetur adipisicing elit. Placeat, voluptatem ab tempore recusandae sequi libero sapiente autem! Sit hic reprehenderit pariatur autem totam, voluptates non officia accusantium rerum unde provident! </p> </div> </div> </div> صفحة الفئة Category ضع ما يلي في ملف المتحكم app/Http/Controllers/CategoryController.php: <?php namespace App\Http\Controllers; use App\Models\Category; use App\Models\Tag; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; class CategoryController extends Controller { /** * عرض قائمة المنشورات التي تنتمي إلى الفئة */ public function category(string $id): View { $category = Category::find($id); $posts = $category->posts()->where('is_published', true)->paginate(env('PAGINATE_NUM')); $categories = Category::all(); $tags = Tag::all(); return view('category', [ 'category' => $category, 'posts' => $posts, 'categories' => $categories, 'tags' => $tags ]); } . . . } ويكون عرض الفئة المقابل resources/views/category.blade.php كما يلي: @extends('layout') @section('title') <title>Category - {{ $category->name }}</title> @endsection @section('content') <div class="grid grid-cols-4 gap-4 py-10"> <div class="col-span-3 grid grid-cols-1">@include('vendor.list')</div> @include('vendor.sidebar') </div> @endsection صفحة الوسم Tag ضع ما يلي في ملف المتحكم app/Http/Controllers/TagController.php: <?php namespace App\Http\Controllers; use App\Models\Category; use App\Models\Tag; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; class TagController extends Controller { /** * عرض قائمة المنشورات التي تنتمي إلى الوسم */ public function tag(string $id): View { $tag = Tag::find($id); $posts = $tag->posts()->where('is_published', true)->paginate(env('PAGINATE_NUM')); $categories = Category::all(); $tags = Tag::all(); return view('tag', [ 'tag' => $tag, 'posts' => $posts, 'categories' => $categories, 'tags' => $tags ]); } . . . } ويكون عرض الوسم المقابل resources/views/tag.blade.php كما يلي: @extends('layout') @section('title') <title>Tag - {{ $tag->name }}</title> @endsection @section('content') <div class="grid grid-cols-4 gap-4 py-10"> <div class="col-span-3 grid grid-cols-1">@include('vendor.list')</div> @include('vendor.sidebar') </div> @endsection صفحة المنشور ضع ما يلي في ملف المتحكم app/Http/Controllers/PostController.php: <?php namespace App\Http\Controllers; use App\Models\Category; use App\Models\Post; use App\Models\Tag; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Facades\Auth; use Illuminate\Database\Eloquent\Builder; class PostController extends Controller { . . . /** * عرض المنشور المطلوب */ public function post(string $id): View { $post = Post::find($id); $categories = Category::all(); $tags = Tag::all(); $related_posts = Post::where('is_published', true)->whereHas('tags', function (Builder $query) use ($post) { return $query->whereIn('name', $post->tags->pluck('name')); })->where('id', '!=', $post->id)->take(3)->get(); return view('post', [ 'post' => $post, 'categories' => $categories, 'tags' => $tags, 'related_posts' => $related_posts ]); } . . . } لاحظ في السطور من 27 إلى 29 أن هذه هي الطريقة التي يمكننا من خلالها استرداد المنشورات ذات الصلة، حيث نريد الحصول على المنشورات التي يكون لها الوسوم نفسها. قد تبدو سلسلة هذه التوابع مخيفة نوعًا ما، لكن لا تقلق بشأن ذلك، حيث سنوضّحها واحدًا تلو الآخر كما يلي: يعيد التابع الأول where('is_published', true)‎ جميع المنشورات التي نشرناها. تتعقد الأمور بعض الشيء عند التابع whereHas()‎، حيث إذا أردنا فهم هذا التابع، فيجب أن نتحدث أولًا عن التابع has()‎، وهو تابع Laravel Eloquent الذي يسمح لنا بالتحقق من وجود علاقة كما في المثال التالي: $posts = Post::has('comments', '>', 3)->get(); ستسترد الشيفرة البرمجية السابقة جميع المنشورات التي تحتوي على أكثر من 3 تعليقات، ولاحظ أنه لا يمكنك استخدام where()‎ لإنجاز ذلك، لأن comments ليس عمودًا في الجدول posts، بل هو جدول آخر له علاقة بالجدول posts. يعمل التابع whereHas()‎ مثل التابع has()‎ تمامًا، ولكنه يوفر مزيدًا من القوة، فالمعامل الثاني هو دالة تسمح بفحص محتوى جدول آخر، وهو الجدول tags في حالتنا، ويمكننا الوصول إلى الجدول tags من خلال المتغير ‎$q. يأخذ التابع whereIn()‎ في السطر 28 معاملَين هما: الأول هو العمود المحدَّد، والثاني هو مصفوفة من القيم المقبولة. يعيد هذا التابع السجلات ذات القيم المقبولة فقط ويستبعد الباقي. يجب أن تكون بقية التوابع سهلة الفهم، حيث يستبعد التابع where('id', '!=', $post->id)‎ المنشور الحالي، ويأخذ التابع take(3)‎ السجلات الثلاثة الأولى. يكون عرض المنشور المقابل resources/views/post.blade.php كما يلي: @extends('layout') @section('title') <title>Page Title</title> @endsection @section('content') <div class="grid grid-cols-4 gap-4 py-10"> <div class="col-span-3"> <img class="rounded-md object-cover h-96 w-full" src="{{ Storage::url($post->cover) }}" alt="..." /> <h1 class="mt-5 mb-2 text-center text-2xl font-bold">{{ $post->title }}</h1> <p class="mb-5 text-center text-sm text-slate-500 italic">By {{ $post->user->name }} | {{ \Carbon\Carbon::parse($post->created_at)->format('M d, Y') }}</p> <div>{!! $post->content !!}</div> <div class="my-5"> @foreach ($post->tags as $tag) <a href="{{ route('tag', ['tag' => $tag->id]) }}" class="text-blue-500 hover:underline" mr-3">#{{ $tag->name }}</a> @endforeach </div> <hr> <!-- المنشورات ذات الصلة --> <div class="grid grid-cols-3 gap-4 my-5"> @foreach ($related_posts as $post) <!-- المنشور --> <div class="mb-4 ring-1 ring-slate-200 rounded-md h-fit hover:shadow-md"> <a href="{{ route('post', ['post' => $post->id]) }}"><img class="rounded-t-md object-cover h-60 w-full" src="{{ Storage::url($post->cover) }}" alt="..." /></a> <div class="m-4 grid gap-2"> <div class="text-sm text-gray-500"> {{ \Carbon\Carbon::parse($post->created_at)->format('M d, Y') }} </div> <h2 class="text-lg font-bold">{{ $post->title }}</h2> <p class="text-base"> {{ Str::limit(strip_tags($post->content), 150, '...') }} </p> <a class="bg-blue-500 hover:bg-blue-700 rounded-md p-2 text-white uppercase text-sm font-semibold font-sans w-fit focus:ring" href="{{ route('post', ['post' => $post->id]) }}">Read more →</a> </div> </div> @endforeach </div> </div> @include('vendor.sidebar') </div> @endsection يُعَد ‎{!! $post->content !!}‎ في السطر 15 هو الوضع الآمن في لارافيل، حيث يخبر لارافيل بعرض وسوم HTML بدلًا من عرضها كنص عادي. صفحة البحث ضع ما يلي في ملف المتحكم app/Http/Controllers/PostController.php: <?php namespace App\Http\Controllers; use App\Models\Category; use App\Models\Post; use App\Models\Tag; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Facades\Auth; use Illuminate\Database\Eloquent\Builder; class PostController extends Controller { . . . /** * عرض نتيجة البحث */ public function search(Request $request): View { $key = $request->input('q'); $posts = Post::where('title', 'like', "%{$key}%")->orderBy('id', 'desc')->paginate(env('PAGINATE_NUM')); $categories = Category::all(); $tags = Tag::all(); return view('search', [ 'key' => $key, 'posts' => $posts, 'categories' => $categories, 'tags' => $tags, ]); } . . . } يكون عرض البحث المقابل resources/views/search.blade.php كما يلي: @extends('layout') @section('title') <title>Search - {{ $key }}</title> @endsection @section('content') <div class="grid grid-cols-4 gap-4 py-10"> <div class="col-span-3 grid grid-cols-1">@include('vendor.list')</div> @include('vendor.sidebar') </div> @endsection ترجمة -وبتصرُّف- للمقال Laravel for Beginners #5 - Create the Frontend لصاحبه Eric Hu. اقرأ أيضًا المقال السابق: لارافيل للمبتدئين-الجزء الرابع: إنشاء لوحة تحكم لموقع مدونة بسيطة تعرف على إطار عمل تطوير الويب الشهير لارافيل Laravel كيف تستخدم منشئ الاستعلامات Query builder للتخاطب مع قاعدة البيانات في Laravel تجريد إعداد قواعد البيانات في لارافيل باستعمال عملية التهجير Migration والبذر Seeder
  14. تُستخدَم المصفوفات في العديد من المجالات لتمثيل كلٍّ من الكائنات والعمليات التي تُطبَّق عليها؛ إذ تُمثَّل النقاط والأشعة بالمصفوفات العمودية في الرسوميات الحاسوبية، وتُمثَّل العمليات عليها بمصفوفات مربّعة. سنناقش في هذا المقال المواضيع التالية: تعريف المصفوفة. عناصر المصفوفة. أبعاد المصفوفة. أسماء المصفوفات. تَساوي مصفوفتين. المصفوفة المربعة. عملية جمع المصفوفات وقواعدها. المصفوفة الصفرية. معاكس مصفوفة. طرح المصفوفات. ضرب المصفوفات بعدد حقيقي Scalar. منقول Transpose المصفوفة. تعريف المصفوفة تتكوّن المصفوفة العمودية من عمود واحد من الأعداد، ولكن يمكن ترتيب هذه الأعداد في عدة صفوف وعدة أعمدة، وبالتالي يمكن وضع الأعداد في مصفوفة ثنائية الأبعاد. إذًا المصفوفة هي مجموعة من الأعداد المرتَّبة ضمن عددٍ ثابت من الصفوف والأعمدة، وتكون هذه الأعداد أعدادًا حقيقية. يمكن أن تحتوي المصفوفات على أعداد معقدة أيضًا، ولكننا لن نراها في هذا المقال. إليك مثالًا لمصفوفة مكونة من ثلاثة صفوف وثلاثة أعمدة: الصف العلوي هو الصف 1، والعمود الموجود في أقصى اليسار هو العمود 1، والمصفوفة السابقة هي مصفوفة بحجم 3x3، لأنها تحتوي على ثلاثة صفوف وثلاثة أعمدة، ويكون التنسيق لوصف المصفوفات هو: rows X columns. يسمّى كل عدد يشكّل المصفوفة عنصرًا Element من عناصرها، ويكون للعناصر مواقع مُحدَّدة في المصفوفة؛ إذ تمثّل الزاويةُ العلوية اليسرى من المصفوفة الصفَّ 1 والعمود 1، وقيمة العنصر الموجود في الصف 1 والعمود 1 في المصفوفة السابقة هي 1، وقيمة العنصر الموجود في الصف 2 والعمود 3 هي 4.6، وقيمة العنصر في الصف 3 والعمود 1 هي 4. أبعاد المصفوفة يُطلَق مصطلح أبعاد المصفوفة على عدد صفوفها وأعمدتها. إليك فيما يلي مصفوفةً مكونةً من ثلاثة صفوف وعمودين: تُكتَب أحيانًا الأبعاد إلى جانب المصفوفة كما هو الحال في المصفوفة السابقة، ولكنه مجرد تذكير صغير وليس جزءًا من المصفوفة. إليك مثالًا آخر لمصفوفة ذات أبعاد مختلفة تتكون من صفين وثلاثة أعمدة، والتي تُعَد "نوع بيانات" مختلف عن المصفوفة السابقة: المصفوفة المربعة Square Matrix تحتوي المصفوفة المربعة Square Matrix على العدد نفسه من الصفوف والأعمدة، وتُستخدَم المصفوفات المربعة لتمثيل التحويلات الهندسية Transformations في الرسوميات الحاسوبية. المصفوفة المستطيلة Rectangular Matrix هي المصفوفة التي يمكن ألّا يكون لها العدد نفسه من الصفوف والأعمدة. مع ذلك، تطالب بعض المراجع بوجوب أن يكون عدد الصفوف مختلفًا عن عدد الأعمدة. أما المصفوفة العمودية Column Matrix فتتكون من عمود واحد، وهي مصفوفة بحجم N x 1، وقد استخدمنا المصفوفة العمودية في هذه السلسلة من المقالات لتمثيل الأشعة الهندسية مثل معظم كتب الرسوميات الحاسوبية. إليك مثالًا عن مصفوفة عمودية بحجم ‎:4 x 1 تتكون المصفوفة السطرية Row Matrix من صفٍ واحد، حيث تستخدم بعض الكتب المصفوفات السطرية لتمثيل الأشعة الهندسية. تسمّي بعض الكتب المصفوفة العمودية بالشعاع العمودي وتسمّي المصفوفة السطرية بالشعاع السطري، ويُعَد ذلك أمرًا جيدًا، ولكنه غامضٌ بعض الشيء، لذا سنستخدم في هذا المقال مصطلح شعاع Vector للتعبير عن كائن هندسي، وسنستخدم المصفوفات العمودية لتمثيل الأشعة، كما سنستخدمها أيضًا لتمثيل النقاط الهندسية. أسماء المصفوفات إذا نسيت ما إذا كانت الصفوف أو الأعمدة تأتي أولًا، فتذكر فقط مصطلح الأعمدة الرومانية ROWman COLumns. يمكن إعطاء اسمٍ للمصفوفة، ويكون هذا الاسم عادةً حرفًا إنجليزيًا كبيرًا بخط عريض في النصوص المطبوعة مثل A أو M، وتُكتَب في بعض الأحيان الأبعاد على يمين الحرف كما في المصفوفة ‎B3x3‎. لعناصر المصفوفة أسماءٌ أيضًا؛ إذ يكون اسم العنصر عادةً بحرفٍ مماثل لاسم المصفوفة، ولكنه حرفٌ صغير مع كتابة موضع العنصر بصورة منخفضة، لذلك يمكن مثلًا كتابة المصفوفة A التي أبعادها 3x3 على النحو التالي: يمكن أيضًا كتابة المصفوفة ‎A = [aij]‎ للقول بأن عناصر المصفوفة A تسمَّى aij‎. تساوي مصفوفتين إذا احتوت مصفوفتان على القيم نفسها، ولكن في مواقع مختلفة، فلا يمكن عَدّ هاتين المصفوفتين متساويتين، فلكي تكون المصفوفتان متساويتان، يجب أن يكون: لهما الأبعاد نفسها. يجب أن تكون العناصر المتقابلة متساوية. لنفترض أن لدينا المصفوفتين ‎A n x m = [aij]‎ و ‎B p x q = [bij]‎، وعندها يكون ‎A = B‎ إذا وفقط إذا كان n=p و m=q وaij=bij ‎ لجميع قيم i و j ضمن المجال المحدَّد. إليك مصفوفتين غير متساويتين بالرغم من أنهما تحتويان على العناصر نفسها: كما أن المصفوفتين التاليتين غير متساويتين، لأن عناصرهما المتقابلة غير متساوية بالرغم من لهما الأبعاد نفسها: جمع المصفوفات إذا كانت لدينا مصفوفتان لهما العدد نفسه من الصفوف والأعمدة، فيمكن حساب مجموعهما؛ حيث إذا كانت A مصفوفةً أبعادها MxN، وكانت B مصفوفةً أبعادها MxN، فإن مجموعهما هو مصفوفة أبعادها MxN مكوّنة من جمع العناصر المتقابلة للمصفوفتين A و B. إليك مثال عن جمع مصفوفتين: تكون عناصر المصفوفات أعدادًا حقيقية ذات كسور عشرية في معظم الحالات العملية، وليست أعدادًا صحيحة صغيرة كتلك المستخدمة غالبًا في أمثلتنا. المصفوفة الصفرية Zero Matrix يمكن جمع مصفوفة أبعادها 3x2 مع مصفوفة أخرى أبعادها 3x2 دون تغيير المصفوفة الثانية إذا كانت جميع عناصر المصفوفة الأولى أصفارًا، والتي تُسمَّى بالمصفوفة الصفرية. إليك مثال عن مصفوفة صفرية أبعادها 3x3: اسم المصفوفة الصفرية هو صفر مكتوب بخط عريض 0، بالرغم من أننا قد ننسى في بعض الأحيان كتابته بخط عريض. أوجد ناتج عملية الجمع التالية: المجموع بالطبع هو المصفوفة غير الصفرية نفسها. قواعد جمع المصفوفات سنوضّح فيما يلي قواعد جمع المصفوفات، إذ يفترض أن تكون للمصفوفات الأبعاد نفسها: A + B = B + A A + 0 = 0 + A = A 0 + 0 = 0 تشبه هذه القواعد بعض القواعد الخاصة بجمع الأعداد الحقيقية، ولكن انتبه إلى أنه ليست جميع قواعد رياضيات المصفوفات مماثلة لتلك الخاصة برياضيات الأعداد الحقيقية. تنص القاعدة الأولى على أن جمع المصفوفات هو عملية تبديلية Commutative، بسبب إجراء عملية الجمع العادي على العناصر المتقابلة في المصفوفتين، ويُعَد الجمع العادي (الحقيقي) تبديليًا. كما يُعَد جمع المصفوفات عملية تجميعية Associative، وهي قاعدة أخرى تماثل قواعد رياضيات الأعداد الحقيقية، بسبب إجراء عملية الجمع العادية على العناصر المتقابلة من المصفوفتين: ‎(A + B) + C = A + (B + C) لنوجِد ناتج جمع المصفوفتين التاليتين: لاحظ أن كل عنصر من عناصر المصفوفة الناتجة التي أبعادها 3x3 هو 10. ضرب المصفوفة بعدد حقيقي Scalar يمكن ضرب المصفوفة بعدد حقيقي (مقدار سلمي) من خلال ضرب كل عنصر من عناصر المصفوفة بهذا العدد. إليك مثال على ذلك، بحيث يكون المتغير a عددًا حقيقيًا: يكون كل عنصر في النتيجة معاكسًا للعنصر الأصل إذا كانت قيمة العدد الحقيقي a في المثال السابق تساوي ‎-1، كما سنرى في الفقرة التالية. معاكس المصفوفة يمكن تشكيل معاكس للمصفوفة من خلال تغيير إشارة كل عنصر من عناصر هذه المصفوفة: ‎-A = -1 A كما في المثال التالي: تنتج المصفوفة الصفرية عن جمع المصفوفة مع معاكسها. لاحظ أن الصفر الأخير هو صفر بخط عريض، والذي يعبّر عن المصفوفة الصفرية: A + (-A) = 0‎ ملاحظة: يمكن تعريف عملية الطرح مصفوفتين على أنها عملية جمع المصفوفة الأولى مع معاكس المصفوفة الثانية. طرح المصفوفات إذا كانت المصفوفتان A و B لهما العدد نفسه من الصفوف والأعمدة، فيمكن تعريف عملية الطرح ‎A - B‎ بأنها ‎A + (-B)‎؛ إذ يمكن حساب ‎A - B‎ من خلال طرح العنصر المقابل من المصفوفة B من كل عنصر من عناصر المصفوفة A. إليك مثال غير مكتمل عن طرح المصفوفات: لاحظ العناصر الموجودة في الصف الأول من الإجابة السابقة؛ إذ تُعَد طريقة حساب النتيجة للعناصر الموجودة في الصف 1 والعمود 2 مربكةً بعض الشيء. لنحاول الآن وضع الإجابة مكان علامتي الاستفهام في المثال السابق كما يلي: منقول مصفوفة Transpose منقول المصفوفة هو مصفوفة جديدة تكون صفوفها هي أعمدة المصفوفة الأصلية، مما يؤدي أيضًا إلى جعل أعمدة المصفوفة الجديدة هي صفوف المصفوفة الأصلية. إليك مثال عن مصفوفة ومنقولها: الحرف المرتفع "T" يعني المنقول "Transpose". هناك طريقة أخرى للنظر إلى المنقول، وهي أن العنصر الموجود في الصف r والعمود c في المصفوفة الأصلية يُوضَع في الصف c والعمود r من المنقول؛ إذ يصبح العنصرُ ‎arc‎ في المصفوفة الأصلية هو العنصر ‎acr‎ في المصفوفة المنقولة. نتعامل عادةً مع المصفوفات المربعة لإيجاد منقولها، ولكن يمكن نقل المصفوفات غير المربعة أيضًا مثل المصفوفة التالية: إليك مثال آخر عن كيفية إيجاد منقول مصفوفة غير مربعة: قاعدة خاصة بمنقول المصفوفة إذا أوجدنا المنقول لمنقول المصفوفة نفسها، فستحصل على المصفوفة الأصلية مرةً أخرى كما في المثال التالي: ويوضح المثال السابق القاعدة: ‎(AT)T = A‎‎. لاحظ أن منقول مصفوفة سطرية هو مصفوفة عمودية، وبالتالي فإن منقول مصفوفة عمودية هو مصفوفة سطرية كما في المثال التالي: المصفوفة المتناظرة Symmetric Matrix المصفوفة المتناظرة هي المصفوفة التي تتساوى مع منقولها، أي: ‎AT = A‎، ولكن يجب أن تكون مصفوفةً مربعة. إليك فيما يلي مثال عن بعض المصفوفات المتناظرة: إذا كانت المصفوفة متناظرة، فستكون عناصرها ‎aij = aji‎. ملاحظة: تُعَد المصفوفة الصفرية مصفوفةً متناظرةً أيضًا. قطر المصفوفة الرئيسي Main Diagonal يتكوّن القطر الرئيسي للمصفوفة من العناصر التي تقع على القطر الذي يمتد من أعلى يسار المصفوفة إلى أسفل يمينها، فإذا كانت لدينا المصفوفة A مثلًا، فإن عناصر قطرها الرئيسي هي العناصر التي يتساوى فيها رقم الصف مع رقم العمود ‎ajj‎. لا يُعَد القطر الآخر للمصفوفة مهمًا وليس له اسم. ملاحظة: عناصر المصفوفة A التي لا تتغير عند تشكيل المنقول ‎AT‎، هي العناصر الموجودة على طول القطر الرئيسي، لأن العناصر ‎ajj‎ في المصفوفة A هي العناصر نفسها في المنقول ‎AT‎. الخلاصة إليك بعض القواعد التي ناقشناها في هذا المقال؛ إذ يجب عليك فهمها بدلًا من حفظها، حيث تحتوي المصفوفات في كل قاعدة من القواعد التالية على العدد نفسه من الصفوف والأعمدة: ‎A + 0 = A‎ ‎A + B = B + A‎ 0 + 0 = ‎0 ‎A + (B + C) = (A + B) + C‎‎ ‎(ab) ‎A = a(b A) ‎a(A + B) = a B + a A‎‎ ‎a 0 = 0‎ ‎(-1) ‎A = -A A - A = 0‎ ‎(AT)T = A 0T = 0‎ حيث أن a و b هي مقادير سلمية (أعداد حقيقية)، و A و B مصفوفتان، و 0 هي المصفوفة الصفرية ذات البعد المناسب. ملاحظة: إذا كان ‎A = B‎ و ‎B = C‎، فإن ‎A = C‎. وصلنا إلى نهاية هذا المقال الذي تعرّفنا فيه على المصفوفات وعملياتها البسيطة، وسنناقش في المقال التالي مزيدًا من الأمور التي يمكن تطبيقها باستخدام المصفوفات. ترجمة -وبتصرُّف- للفصل Matrices and Simple Matrix Operations من كتاب Vector Math for 3D Computer Graphics لصاحبه Bradley Kjell. اقرأ أيضًا المقال السابق: الجداء الشعاعي في التصاميم ثلاثية الأبعاد وخاصياته وحسابه كيفية جمع وطرح المصفوفات العمودية والمصفوفات السطرية الأشعة والنقاط والمصفوفات العمودية في الرسوميات الحاسوبية ثلاثية الأبعاد.
  15. سننشئ في هذا المقال تطبيق مدونة باستخدام لارافيل ونجعل تطبيقنا كامل الميزات ويحتوي على منشورات وفئات Categories ووسوم Tags، حيث ناقشنا في المقال السابق عمليات CRUD للمنشورات، وسنكرر اليوم هذه العمليات نفسها على كل من الفئات والوسوم، كما سنناقش كيفية التعامل مع العلاقات فيما بينها. تهيئة مشروع لارافيل جديد سنبدأ بمشروع لارافيل جديد، حيث سننشئ مجلد العمل وننتقل إليه، ولكن تأكّد من تشغيل دوكر Docker، ثم نفّذ الأمر التالي: curl -s https://laravel.build/<app_name> | bash انتقل إلى مجلد التطبيق وابدأ تشغيل الخادم كما يلي: cd <app_name> ./vendor/bin/sail up لننشئ اسمًا بديلًا للأداة sail لتسهيل الأمور، لذا شغّل الأمر التالي: alias sail='[ -f sail ] && sh sail || sh vendor/bin/sail' يمكنك من الآن فصاعدًا تشغيل الأداة Sail مباشرةً دون تحديد المسار بأكمله كما يلي: sail up استيثاق المستخدم User authentication يأتي إطار عمل لارافيل مع نظام بيئي كبير فهو يحتوي على العديد من الأدوات والموارد الإضافية التي تسهل على المطورين برمجة التطبيقات، وتُعَد حزمة Breeze من لارافيل جزءًا من هذا النظام، فهي توفر طريقة سريعة لإعداد استيثاق المستخدم وتسجيله في تطبيق لارافيل. تتضمّن حزمة Breeze عروضًا Views ومتحكّمات Controllers مبنية مسبقًا خاصة بالاستيثاق، بالإضافة إلى مجموعة من واجهات برمجة التطبيقات الخلفية للتعامل مع استيثاق المستخدم وتسجيله، وصُمِّمت هذه الحزمة لتكون سهلة التثبيت والضبط مع وجود الحد الأدنى من الإعداد المطلوب. استخدم الأوامر التالية لتثبيت حزمة Breeze من لارافيل: sail composer require laravel/breeze --dev sail artisan breeze:install sail artisan migrate sail npm install sail npm run dev ستولّد هذه العملية تلقائيًا كل من المتحكمات والبرمجيات الوسيطة والعروض المطلوبة اللازمة لإنشاء نظام استيثاق مستخدم أساسي في تطبيقك. يمكنك الوصول إلى صفحة التسجيل من العنوان http://127.0.0.1/register، ثم تسجّل حسابًا جديدًا وسيُعاد توجيهك إلى لوحة التحكم. لن نناقش في هذا المقال كيفية عمل نظام استيثاق المستخدم، لأنه يرتبط ببعض المفاهيم المتقدمة إلى حدٍ ما، ولكن يوصَى بشدة بإلقاء نظرة على الملفات التي تولّدت هنا، فهي توفر نظرة أعمق حول كيفية العمل في لارافيل. إعداد قاعدة البيانات يجب بعد ذلك أن يكون لدينا تصور حول كيفية ظهور تطبيق المدونة، حيث نحتاج أولًا إلى قاعدة بيانات يمكنها تخزين المنشورات والفئات والوسوم، وسيكون لكل جدول قاعدة بيانات البنيةُ التالية: جدول المنشورات Posts: المفتاح نوعه id عدد صحيح كبير BigInteger created_at timestamps يملأ تلقائيًا عند إنشاء سجل جديد updated_at timestamps يملأ تلقائيًا عند تحديث سجل جديد title سلسلة نصية String cover سلسلة نصية String content نص Text is_published قيمة منطقية Boolean جدول الفئات Categories: المفتاح نوعه id عدد صحيح كبير created_at timestamps يملأ تلقائيًا عند إنشاء سجل جديد updated_at timestamps يملأ تلقائيًا عند تحديث سجل جديد name سلسلة نصية String جدول الوسوم Tags: المفتاح نوعه id عدد صحيح كبير created_at timestamps يملأ تلقائيًا عند إنشاء سجل جديد updated_at timestamps يملأ تلقائيًا عند إنشاء سجل جديد name سلسلة نصية يجب أن يكون هناك أيضًا جدول للمستخدمين users، ولكن لاحاجة لأن ننشئه فقد ولّدته حزمة Breeze من لارافيل مسبقًا، لذا سنتخطى هذه الخطوة حاليًا. يكون لهذه الجداول علاقات مع بعضها البعض كما هو موضح فيما يلي: كل مستخدم لديه منشورات متعددة. كل فئة لديها العديد من المنشورات. كل وسم لديه العديد من المنشورات. يعودة كل منشور إلى مستخدم واحد. يعود كل منشور إلى فئة واحدة. كل منشور له العديد من الوسوم. يمكننا إنشاء هذه العلاقات، ولكن يجب تعديل جدول المنشورات كما يلي: جدول المنشورات مع العلاقات: المفتاح نوعه id عدد صحيح كبير created_at timestamps يملأ تلقائيًا عند إنشاء سجل جديد updated_at timestamps يملأ تلقائيًا عند تحديث سجل جديد title سلسلة نصية String cover سلسلة نصية String content نص Text is_published قيمة منطقية user_id عدد صحيح كبير category_id عدد صحيح كبير ونحتاج أيضًا إلى جدول منفصل لعلاقة المنشور مع الوسم كما يلي: جدول يمثل علاقة المنشور مع الوسم Post/Tag: المفتاح نوعه post_id عدد صحيح كبير tag_id عدد صحيح كبير تطبيق بنية قاعدة البيانات يمكن تطبيق التصميم السابق من خلال توليد النماذج Models وملفات التهجير Migration باستخدام الأوامر التالية: sail artisan make:model Post --migration sail artisan make:model Category --migration sail artisan make:model Tag --migration بالإضافة إلى توليد ملف تهجير منفصل للجدول post_tag باستخدام الأمر التالي: sail artisan make:migration create_post_tag_table وسينشأ ملف التهجير database/migrations/create_posts_table.php التالي: <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { /** * تشغيل عمليات التهجير */ public function up(): void { Schema::create('posts', function (Blueprint $table) { $table->id(); $table->timestamps(); $table->string('title'); $table->string('cover'); $table->text('content'); $table->boolean('is_published'); $table->bigInteger('user_id'); $table->bigInteger('category_id'); }); } /** * عكس عمليات التهجير */ public function down(): void { Schema::dropIfExists('posts'); } }; وسينشأ ملف التهجير database/migrations/create_categories_table.php التالي: <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { /** * تشغيل عمليات التهجير */ public function up(): void { Schema::create('categories', function (Blueprint $table) { $table->id(); $table->timestamps(); $table->string('name'); }); } /** * عكس عمليات التهجير */ public function down(): void { Schema::dropIfExists('categories'); } }; وسينشأ ملف التهجير database/migrations/create_tags_table.php التالي: <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { /** * تشغيل عمليات التهجير */ public function up(): void { Schema::create('tags', function (Blueprint $table) { $table->id(); $table->timestamps(); $table->string('name'); }); } /** * عكس عمليات التهجير */ public function down(): void { Schema::dropIfExists('tags'); } }; وسينشأ أيضُا ملف التهجير database/migrations/create_post_tag_table.php التالي: <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { /** * تشغيل عمليات التهجير */ public function up(): void { Schema::create('post_tag', function (Blueprint $table) { $table->id(); $table->timestamps(); $table->bigInteger('post_id'); $table->bigInteger('tag_id'); }); } /** * عكس عمليات التهجير */ public function down(): void { Schema::dropIfExists('post_tag'); } }; طبّق هذه التغييرات باستخدام الأمر التالي: sail artisan migrate يجب بعد ذلك تفعيل الإسناد الجماعي Mass Assignment لحقول محدَّدة بالنسبة للنماذج المقابلة حتى نتمكّن من استخدام تابعَي create أو update معها كما ناقشنا في المقال السابق، ويجب أيضًا تعريف العلاقات بين جداول قاعدة البيانات. سيكون ملف النموذج app/Models/Post.php كما يلي: <?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; class Post extends Model { use HasFactory; protected $fillable = [ "title", 'content', 'cover', 'is_published' ]; public function user(): BelongsTo { return $this->belongsTo(User::class); } public function category(): BelongsTo { return $this->belongsTo(Category::class); } public function tags(): BelongsToMany { return $this->belongsToMany(Tag::class); } } وسيكون ملف النموذج app/Models/Category.php كما يلي: <?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; class Category extends Model { use HasFactory; protected $fillable = [ 'name', ]; public function posts(): HasMany { return $this->hasMany(Post::class); } } ويكون ملف النموذج app/Models/Tag.php كما يلي: <?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; class Tag extends Model { use HasFactory; protected $fillable = [ 'name', ]; public function posts(): BelongsToMany { return $this->belongsToMany(Post::class); } } ويكون ملف النموذج app/Models/User.php كما يلي: <?php namespace App\Models; // use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Laravel\Sanctum\HasApiTokens; class User extends Authenticatable { use HasApiTokens, HasFactory, Notifiable; /** * السمات‫ Attributes التي نسندها إسنادًا جماعيًا * * @var array<int, string> */ protected $fillable = [ 'name', 'email', 'password', ]; /** * ‫السمات التي يجب أن تكون مخفية لعملية السَلسلة Serialization * * @var array<int, string> */ protected $hidden = [ 'password', 'remember_token', ]; /** * السمات التي يجب تغيير نوعها‫ Cast * * @var array<string, string> */ protected $casts = [ 'email_verified_at' => 'datetime', ]; public function posts(): HasMany { return $this->hasMany(Post::class); } } المتحكمات Controllers والوِجهات Routes نحتاج إلى إنشاء متحكم موارد واحد لكل مورد (منشور وفئة ووسم) باستخدام الأوامر التالية: php artisan make:controller PostController --resource php artisan make:controller CategoryController --resource php artisan make:controller TagController --resource ننشئ بعد ذلك وِجهات لكل من هذه المتحكمات في الملف routes/web.php كما يلي: <?php use App\Http\Controllers\CategoryController; use App\Http\Controllers\PostController; use App\Http\Controllers\ProfileController; use App\Http\Controllers\TagController; use Illuminate\Support\Facades\Route; // وِجهات لوحة التحكم Route::prefix('dashboard')->group(function () { // الصفحة الرئيسية للوحة التحكم Route::get('/', function () { return view('dashboard'); })->name('dashboard'); // مورد الفئة للوحة التحكم Route::resource('categories', CategoryController::class); // مورد الوسم للوحة التحكم Route::resource('tags', TagController::class); // مورد المنشور للوحة التحكم Route::resource('posts', PostController::class); })->middleware(['auth', 'verified']); Route::middleware('auth')->group(function () { Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); }); require __DIR__ . '/auth.php'; لاحظ تجميع جميع الوِجهات باستخدام البادئة ‎/dashboard، ويكون للمجموعة البرمجيةُ الوسيطة auth، مما يعني أنه يجب على المستخدم تسجيل الدخول للوصول إلى لوحة التحكم. متحكمات الفئة والوسم Category/Tag يُعََد المتحكمان CategoryController و TagController واضحَين إلى حدٍ ما، ويمكنك إعدادهما بالطريقة نفسها التي أنشأنا بها المتحكم PostController في المقال السابق. إذًا لننشئ أولًا متحكم الفئة app/Http/Controllers/CategoryController.php كما يلي: <?php namespace App\Http\Controllers; use App\Models\Category; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; class CategoryController extends Controller { /** * عرض قائمة الموارد */ public function index(): View { $categories = Category::all(); return view('categories.index', [ 'categories' => $categories ]); } /** * عرض الاستمارة الخاصة بإنشاء مورد جديد */ public function create(): View { return view('categories.create'); } /** * تخزين المورد الذي أنشأناه حديثًا في وحدة التخزين */ public function store(Request $request): RedirectResponse { // الحصول على البيانات من الطلب $name = $request->input('name'); // إنشاء نسخة جديدة من المنشور‫ Post ووضع البيانات المطلوبة في العمود المقابل $category = new Category(); $category->name = $name; // حفظ البيانات $category->save(); return redirect()->route('categories.index'); } /** * عرض المورد المُحدَّد */ public function show(string $id): View { $category = Category::all()->find($id); $posts = $category->posts(); return view('categories.show', [ 'category' => $category, 'posts' => $posts ]); } /** * عرض الاستمارة الخاصة بتعديل المورد المُحدَّد */ public function edit(string $id): View { $category = Category::all()->find($id); return view('categories.edit', [ 'category' => $category ]); } /** * تحديث المورد المُحدَّد في وحدة التخزين */ public function update(Request $request, string $id): RedirectResponse { // الحصول على البيانات من الطلب $name = $request->input('name'); // البحث عن الفئة المطلوبة ووضع البيانات المطلوبة في العمود المقابل $category = Category::all()->find($id); $category->name = $name; // حفظ البيانات $category->save(); return redirect()->route('categories.index'); } /** * إزالة المورد المٌحدَّد من وحدة التخزين */ public function destroy(string $id): RedirectResponse { $category = Category::all()->find($id); $category->delete(); return redirect()->route('categories.index'); } } ولننشئ الآن متحكم الوسم app/Http/Controllers/TagController.php كما يلي: <?php namespace App\Http\Controllers; use App\Models\Tag; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; class TagController extends Controller { /** * عرض قائمة الموارد */ public function index(): View { $tags = Tag::all(); return view('tags.index', [ 'tags' => $tags ]); } /** * عرض الاستمارة الخاصة بإنشاء مورد جديد */ public function create(): View { return view('tags.create'); } /** * تخزين المورد الذي أنشأناه حديثًا في وحدة التخزين */ public function store(Request $request): RedirectResponse { // الحصول على البيانات من الطلب $name = $request->input('name'); // إنشاء نسخة جديدة من المنشور‫ Post ووضع البيانات المطلوبة في العمود المقابل $tag = new Tag(); $tag->name = $name; // حفظ البيانات $tag->save(); return redirect()->route('tags.index'); } /** * عرض المورد المُحدَّد */ public function show(string $id): View { $tag = Tag::all()->find($id); $posts = $tag->posts(); return view('tags.show', [ 'tag' => $tag, 'posts' => $posts ]); } /** * عرض الاستمارة الخاصة بتعديل المورد المُحدَّد */ public function edit(string $id): View { $tag = Tag::all()->find($id); return view('tags.edit', [ 'tag' => $tag ]); } /** * تحديث المورد المُحدَّد في وحدة التخزين */ public function update(Request $request, string $id): RedirectResponse { // الحصول على البيانات من الطلب $name = $request->input('name'); // البحث عن الوسم المطلوب ووضع البيانات المطلوبة في العمود المقابل $tag = Tag::all()->find($id); $tag->name = $name; // حفظ البيانات $tag->save(); return redirect()->route('tags.index'); } /** * إزالة المورد المُحدَّد من وحدة التخزين */ public function destroy(string $id): RedirectResponse { $tag = Tag::all()->find($id); $tag->delete(); return redirect()->route('tags.index'); } } تذكّر أنه يمكنك التحقق من اسم الوِجهات باستخدام الأمر التالي: sail artisan route:list متحكم المنشور Post يُعَد متحكم المنشور PostController أكثر تعقيدًا بعض الشيء، إذ يجب عليك التعامل مع عمليات رفع الصور والعلاقات بين الجداول في قاعدة البيانات في التابع store()‎. إذًا لننشئ هذا المتحكم app/Http/Controllers/PostController.php كما يلي: <?php namespace App\Http\Controllers; use App\Models\Category; use App\Models\Post; use App\Models\Tag; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Facades\Auth; use Illuminate\Database\Eloquent\Builder; class PostController extends Controller { . . . /** * تخزين المورد الذي أنشأناه حديثًا في وحدة التخزين */ public function store(Request $request): RedirectResponse { // الحصول على البيانات من الطلب $title = $request->input('title'); $content = $request->input('content'); if ($request->input('is_published') == 'on') { $is_published = true; } else { $is_published = false; } // ‫إنشاء نسخة جديدة من المنشور Post ووضع البيانات المطلوبة في العمود المقابل $post = new Post(); $post->title = $title; $post->content = $content; $post->is_published = $is_published; // حفظ صورة الغلاف $path = $request->file('cover')->store('cover', 'public'); $post->cover = $path; // ضبط المستخدم $user = Auth::user(); $post->user()->associate($user); // ضبط الفئة $category = Category::find($request->input('category')); $post->category()->associate($category); // حفظ المنشور $post->save(); // ضبط الوسوم $tags = $request->input('tags'); foreach ($tags as $tag) { $post->tags()->attach($tag); } return redirect()->route('posts.index'); } . . . } هناك بعض الأشياء التي يجب ملاحظتها في التابع store()‎، حيث سنستخدم في الأسطر من 28 إلى 32 مربع اختيار HTML لتمثيل الحقل is_published، وتكون قيمته إما 'on' أو null، ولكن تُحفَظ قيمته في قاعدة البيانات بوصفها true أو false، لذلك يجب استخدام التعليمة if لحل هذه المشكلة. يمكننا استرداد الملفات في السطور من 41 إلى 42 من خلال استخدام التابع file()‎ بدلًا من التابع input()‎، ويُحفظ الملف في القرص public ضمن المجلد cover. نحصل في السطور من 45 إلى 46 على المستخدم الحالي باستخدام التابع Auth::user()‎، ونربط المنشور بالمستخدم باستخدام التابع associate()‎، وتفعَل السطور من 49 إلى 50 الشيء نفسه بالنسبة للفئة، وتذكّر أنه يمكنك تطبيق ذلك فقط مع المتغير ‎$post وليس ‎$user أو ‎$category، لأن العمودان user_id و category_id موجودان في الجدول posts. وأخيرًا، يجب حفظ المنشور الحالي في قاعدة البيانات، ثم استرداد قائمة الوسوم وإرفاق كل منها بالمنشور واحدًا تلوَ الآخر باستخدام التابع attach()‎ بالنسبة للوسوم كما هو موضح في السطور من 56 إلى 60. تجري الأمور بطريقة مشابهة بالنسبة إلى التابع update()‎، باستثناء أنه يجب إزالة جميع الوسوم الموجودة قبل أن تتمكّن من إرفاق الوسوم الجديدة كما يلي: $post->tags()->detach(); العروض Views تذكر دائمًا أن تكون منظّمًا عند إنشاء نظام عرض، حيث سنتّبع البنية التالية في مثالنا: resources/views ├── auth ├── categories │ ├── create.blade.php │ ├── edit.blade.php │ ├── index.blade.php │ └── show.blade.php ├── components ├── layouts ├── posts │ ├── create.blade.php │ ├── edit.blade.php │ ├── index.blade.php │ └── show.blade.php ├── profile ├── tags │ ├── create.blade.php │ ├── edit.blade.php │ ├── index.blade.php │ └── show.blade.php ├── dashboard.blade.php └── welcome.blade.php أنشأنا ثلاثة مجلدات هي: posts و categories و tags، ولكل منها أربعة قوالب هي: create و edit و index و show باستثناء المجلد posts، لأن وجود صفحة show للمنشورات في لوحة التحكم أمر غير ضروري. يؤدي تضمين جميع هذه العروض في مقال واحد إلى جعل هذا المقال طويلًا بلا داعٍ، لذا سنوضّح فقط صفحات إنشاء وتعديل وفهرس المنشورات، ولكن يمكنك الاطلاع على الشيفرة المصدرية الكاملة على Github. عرض إنشاء منشور لننشئ أولًا عرض إنشاء المنشور resources/views/posts/create.blade.php كما يلي: <x-app-layout> <x-slot name="header"> <div class="flex justify-between"> <h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight" > {{ __('Posts') }} </h2> <a href="{{ route('posts.create') }}"> <x-primary-button>{{ __('New') }}</x-primary-button> </a> </div> <script src="https://cdn.tiny.cloud/. . ./tinymce.min.js" referrerpolicy="origin" ></script> </x-slot> <div class="py-12"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> <div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg"> <div class=""> <form action="{{ route('posts.store') }}" method="POST" class="mt-6 space-y-3" enctype="multipart/form-data" > {{ csrf_field() }} <input type="checkbox" name="is_published" id="is_published" /> <x-input-label for="is_published" >Make this post public</x-input-label > <br /> <x-input-label for="title">{{ __('Title') }}</x-input-label> <x-text-input id="title" name="title" type="text" class="mt-1 block w-full" required autofocus autocomplete="name" /> <br /> <x-input-label for="content">{{ __('Content') }}</x-input-label> <textarea name="content" id="content" cols="30" rows="30" ></textarea> <br /> <x-input-label for="cover">{{ __('Cover Image') }}</x-input-label> <x-text-input id="cover" name="cover" type="file" class="mt-1 block w-full" required autofocus autocomplete="cover" /> <br /> <x-input-label for="category">{{ __('Category') }}</x-input-label> <select id="category" name="category"> @foreach($categories as $category) <option value="{{ $category->id }}">{{ $category->name }}</option> @endforeach </select> <br /> <x-input-label for="tags">{{ __('Tags') }}</x-input-label> <select id="tags" name="tags[]" multiple> @foreach($tags as $tag) <option value="{{ $tag->id }}">{{ $tag->name }}</option> @endforeach </select> <br /> <x-primary-button>{{ __('Save') }}</x-primary-button> </form> <script> tinymce.init({. . .}); </script> </div> </div> </div> </div> </x-app-layout> استخدمنا في مثالنا محرّر النصوص TinyMCE، ولكن يمكنك استخدام محرّر نصوص آخر عوضًا عنه، أو استخدم العنصر <textarea></textarea> إذا أدرتَ ذلك. يجب أن تحتوي الاستمارة الموجودة في السطر 24 على السمة التي هي enctype="multipart/form-data"‎ لأننا لا ننقل النصوص فحسب، بل توجد ملفات أيضًا. تذكر في السطر 59 استخدام السمة type="file"‎ لأننا نرفع صورة، وستُنقَل قيمة الخيار في السطور من 67 إلى 71 إلى الواجهة الخلفية. هناك شيئان يجب الانتباه إليهما في الأسطر من 74 إلى 78، فلاحظ أولًا السمة name="tags[]"‎، حيث تخبر هذه الأقواس [] لارافيل بنقل مصفوفة قابلة للتكرار بدلًا من النصوص. ثانيًا، تنشئ السمة multiple استمارة متعددة التحديد بدلًا من تحديد فردي مثل استمارة الفئات. عرض تعديل المنشور لننشئ الآن العرض resources/views/posts/edit.blade.php كما يلي: <x-app-layout> <x-slot name="header"> <div class="flex justify-between"> <h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight" > {{ __('Posts') }} </h2> <a href="{{ route('posts.create') }}"> <x-primary-button>{{ __('New') }}</x-primary-button> </a> </div> <script src="https://cdn.tiny.cloud/. . ./tinymce.min.js" referrerpolicy="origin" ></script> </x-slot> <div class="py-12"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> <div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg"> <div class=""> <form action="{{ route('posts.update', ['post' => $post->id]) }}" method="POST" class="mt-6 space-y-3" enctype="multipart/form-data" > {{ csrf_field() }} {{ method_field('PUT') }} <input type="checkbox" name="is_published" id="is_published" @checked($post- />is_published)/> <x-input-label for="is_published" >Make this post public</x-input-label > <br /> <x-input-label for="title">{{ __('Title') }}</x-input-label> <x-text-input id="title" name="title" type="text" class="mt-1 block w-full" required autofocus autocomplete="name" value="{{ $post->title }}" /> <br /> <x-input-label for="content">{{ __('Content') }}</x-input-label> <textarea name="content" id="content" cols="30" rows="30"> {{ $post->content }}</textarea > <br /> <x-input-label for="cover" >{{ __('Update Cover Image') }}</x-input-label > <img src="{{ Illuminate\Support\Facades\Storage::url($post->cover) }}" alt="cover image" width="200" /> <x-text-input id="cover" name="cover" type="file" class="mt-1 block w-full" autofocus autocomplete="cover" /> <br /> <x-input-label for="category">{{ __('Category') }}</x-input-label> <select id="category" name="category"> @foreach($categories as $category) <option value="{{ $category->id }}" @selected($post-> category->id == $category->id)>{{ $category->name }} </option> @endforeach </select> <br /> <x-input-label for="tags">{{ __('Tags') }}</x-input-label> <select id="tags" name="tags[]" multiple> @foreach($tags as $tag) <option value="{{ $tag->id }}" @selected($post-> tags->contains($tag))>{{ $tag->name }} </option> @endforeach </select> <br /> <x-primary-button>{{ __('Save') }}</x-primary-button> </form> <script> tinymce.init({. . .}); </script> </div> </div> </div> </div> </x-app-layout> لاحظ في السطور من 24 إلى 26 أن لغة HTML لا تدعم التابع PUT افتراضيًا، لذا نستخدم السمة method="POST"‎ ثم نخبر لارافيل باستخدام التابع PUT من خلال التعليمة ‎{{ method_field('PUT') }}‎. عرض فهرس المنشورات لننشئ الآن العرض resources/views/posts/index.blade.php كما يلي: <x-app-layout> <x-slot name="header"> <div class="flex justify-between"> <h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight" > {{ __('Posts') }} </h2> <a href="{{ route('posts.create') }}"> <x-primary-button>{{ __('New') }}</x-primary-button> </a> </div> </x-slot> <div class="py-12"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> @foreach($posts as $post) <div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg mb-4 px-4 h-20 flex justify-between items-center" > <div class="text-gray-900 dark:text-gray-100"> <p>{{ $post->title }}</p> </div> <div class="space-x-2"> <a href="{{ route('posts.edit', ['post' => $post->id]) }}"> <x-primary-button>{{ __('Edit') }}</x-primary-button></a > <form method="post" action="{{ route('posts.destroy', ['post' => $post->id]) }}" class="inline" > {{ csrf_field() }} {{ method_field('DELETE') }} <x-danger-button> {{ __('Delete') }} </x-danger-button> </form> </div> </div> @endforeach </div> </div> </x-app-layout> لاحظ أن زر الحذف ليس زرًا عاديًا، إذ يجب أن يكون استمارة مع تابع DELETE، لأن الرابط العادي يمتلك التابع GET فقط. يُفترَض الآن أن تكون قادرًا على إنشاء بقية نظام العرض بسهولة. لقطات الشاشة وأخيرًا، إليك بعض لقطات الشاشة للوحة التحكم التي أنشأناها: صفحة إدارة التصنيفات: ننتقل لصفحة الإدارة كما يلي: ثم نختار التصنيفات Categories من الصورة أعلاه لننتقل للصفحة التالية لإدارة التصنيفات: صفحة إنشاء تصنيف جديد: صفحة تحديث المنشور: تابع معنا المقال التالي والأخير من هذه السلسلة حيث سننشئ فيه كل ما يخص الواجهة الأمامية من التطبيق كي يتمكن المستخدمون من رؤية منشورات المدونة والتفاعل معها. ترجمة -وبتصرُّف- للمقال Laravel for Beginners #4 - Create a Dashboard لصاحبه Eric Hu. اقرأ أيضًا المقال السابق: لارافيل للمبتدئين-الجزء الثالث: استخدام عمليات CRUD لإنشاء مدونة بسيطة تجريد إعداد قواعد البيانات في لارافيل باستعمال عملية التهجير Migration والبذر Seeder كيف تستخدم منشئ الاستعلامات Query builder للتخاطب مع قاعدة البيانات في Laravel إنشاء استمارة اتصال في Laravel باستخدام ميزة Form Request
  16. سنستخدم في هذا المقال كل ما تعلّمناه في المقالين السابقين من سلسلة مقالات لارافيل للمبتدئين لإنشاء مشروع حقيقي، حيث سننشئ مدونة صغيرة تحتوي على منشورات فقط بدون فئات Categories أو وسوم Tags، وكل منشور له عنوان ومحتوى، إذ لن ننشئ تطبيق مدونة كامل المواصفات، وسنوضّح كيفية استرداد البيانات من قاعدة البيانات، وكيفية إنشاء أو تحديث معلومات جديدة، وكيفية حفظها في قاعدة البيانات، وكيفية حذفها، حيث يمثّل الاختصار CRUD عمليات الإنشاء ‎Create والقراءة ‎Read والتحديث ‎Update والحذف ‎Delete. تصميم بنية قاعدة البيانات سننشئ أولًا ملف تهجير Migration للجدول post، ويجب أن يحتوي هذا الجدول على العنوان والمحتوى، ويمكنك توليد ملف تهجير باستخدام الأمر التالي: php artisan make:migration create_posts_table وسيتضمن الملف ما يلي: <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { /** * تشغيل عمليات التهجير */ public function up(): void { Schema::create('posts', function (Blueprint $table) { $table->id(); $table->timestamps(); $table->string('title'); $table->text('content'); }); } /** * عكس عمليات التهجير */ public function down(): void { Schema::dropIfExists('posts'); } }; أنشأنا في هذا المثال خمسة أعمدة في الجدول post باستدعاء 5 توابع وهي: ينشئ التابع id()‎ عمود المعرّف id الذي يُستخدَم عادة للفهرسة. ينشئ التابع timestamps()‎ عمودين هما: created_at و uptated_at، وسيُحدَّث هذان العمودان تلقائيًا عند إنشاء السجل وتحديثه. ينشئ التابع string('title')‎ العمود title الذي نوعه VARCHAR ويبلغ طوله الافتراضي 255 بايتًا. ينشئ التابع string('content')‎ عمود المحتوى content. يمكنك تطبيق التغييرات من خلال تشغيل الأمر التالي: php artisan migrate ويجب أن ينشأ جدول posts جديد كما يلي: يمكننا الآن إنشاء النموذج Model المقابل لهذا الجدول باستخدام الأمر التالي: php artisan make:model Post وسيتضمّن هذا النموذج app/Models/Post.php ما يلي: <?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Post extends Model { use HasFactory; } لننشئ الآن متحكم الموارد Resource Controller المقابل باستخدام الأمر التالي: php artisan make:controller PostController --resource وأخيرًا، سجّل هذا المتحكم في الموجِّه Router كما يلي: use App\Http\Controllers\PostController; Route::resource('posts', PostController::class); يمكنك التحقق من الوِجهات Routes المُسجَّلة من خلال تشغيل الأمر التالي: php artisan route:list وسيظهر الخرج التالي: GET|HEAD posts ................................................................. posts.index › PostController@index POST posts ................................................................. posts.store › PostController@store GET|HEAD posts/create ........................................................ posts.create › PostController@create GET|HEAD posts/{post} ............................................................ posts.show › PostController@show PUT|PATCH posts/{post} ........................................................ posts.update › PostController@update DELETE posts/{post} ...................................................... posts.destroy › PostController@destroy GET|HEAD posts/{post}/edit ....................................................... posts.edit › PostController@edit يحتوي الخرج السابق على معلومات مثل توابع الطلبات Request Methods وتوابع المتحكّمات Controller Methods بالإضافة إلى أسماء الوِجهات، وتُعَد هذه المعلومات مهمة جدًا وسنحتاج إليها لاحقًا في هذا المقال. عمليات CRUD حان الوقت الآن لنتعمّق في التطبيق نفسه، فمن غير المحتمل أن ننشئ جميع المتحكمات أولًا ثم نصمّم القوالب ثم ننتقل إلى الموجّهات عند إنشاء تطبيقات واقعية، إذ يجب أن نفكّر من وجهة نظر المستخدم وفي الإجراءات التي قد يرغب المستخدم في اتخاذها. يجب أن يكون لدى المستخدم القدرة على إجراء أربع عمليات لكل مورد، والذي هو Post في حالتنا، وهذه العمليات هي: الإنشاء Create: يجب أن يكون المستخدم قادرًا على إنشاء موارد جديدة وحفظها في قاعدة البيانات. القراءة Read: يجب أن يكون المستخدم قادرًا على قراءة الموارد سواءً من خلال استرداد قائمة الموارد أو التحقق من تفاصيل مورد محدّد. التحديث Update: يجب أن يكون المستخدم قادرًا على تحديث الموارد الموجودة وتحديث سجل قاعدة البيانات المقابلة لها. الحذف Delete: يجب أن يكون المستخدم قادرًا على إزالة مورد من قاعدة البيانات. ويشار إلى هذه العمليات مع بعضها البعض باسم عمليات CRUD. إجراء الإنشاء Create لا تزال قاعدة بياناتنا فارغة حاليًا، لذا قد يرغب المستخدم في إنشاء منشورات جديدة، فلنبدأ بإجراء الإنشاء (C)، حيث يمكن إكمال هذا الإجراء من خلال استخدام التابعين التاليين: تابع المتحكم create()‎ الذي يعرض استمارة، مما يسمح للمستخدم بملء العنوان والمحتوى. تابع المتحكم store()‎ الذي يحفظ المنشور الذي أنشأناه حديثًا في قاعدة البيانات، ويعيد توجيه المستخدم إلى صفحة القائمة. يطابق التابع create()‎ نمط URL الذي هو ‎/posts/create (التابع GET)، ويطابق التابع store()‎ نمط URL الذي هو ‎/post (التابع POST). إليك مراجعةً مختصرة لتوابع HTTP في حال احتياجك إلى تجديد بعض المعلومات: تابع GET هو تابع طلبات HTTP الأكثر استخدامًا، ويُستخدَم لطلب البيانات والموارد من الخادم. يُستخدَم تابع POST لإرسال البيانات إلى الخادم، ويُستخدَم لإنشاء أو تحديث المورد. يعمل تابع HEAD مثل تابع GET تمامًا، باستثناء أن استجابة HTTP ستحتوي على الترويسة فقط بدون الجسم، ويستخدم المطورون هذا التابع لأغراض تنقيح الأخطاء Debugging. يُعَد تابع PUT مشابهًا لتابع POST، مع اختلاف واحد بسيط، حيث إذا أرسلتَ باستخدام التابع POST موردًا موجودًا مسبقًا على الخادم، فلن يسبّب هذا الإجراء أيّ فرق، ولكن سيكرّر تابع PUT هذا المورد في كل مرة نقدّم فيها الطلب. يزيل تابع DELETE موردًا من الخادم. لنبدأ بالتابع create()‎، حيث سيتضمّن ملف المتحكم app/Http/Controllers/PostController.php ما يلي: <?php namespace App\Http\Controllers; use App\Models\Post; use Illuminate\Contracts\View\View; . . . class PostController extends Controller { . . . /** * عرض الاستمارة الخاصة بإنشاء مورد جديد */ public function create(): View { return view('posts.create'); } . . . } سيُنفّذ هذا التابع عندما ترسل طلب GET إلى ‎/posts/create، وسيؤشّر إلى العرض views/posts/create.blade.php. لاحظ تغيير الاستجابة Response إلى عرض View في السطر 16، لأن هذه التوابع يجب أن تعيد عرضًا. يجب بعد ذلك أن ننشئ العرض المقابل، حيث سنبدأ بعرض تخطيط الصفحة layout، إذ سيتضمّن الملف views/layout.blade.php ما يلي: <!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <script src="https://cdn.tailwindcss.com"></script> @yield('title') </head> <body class="container mx-auto font-serif"> <div class="bg-white text-black font-serif"> <div id="nav"> <nav class="flex flex-row justify-between h-16 items-center shadow-md"> <div class="px-5 text-2xl"> <a href="{{ route('posts.index') }}"> My Blog </a> </div> <div class="hidden lg:flex content-between space-x-10 px-10 text-lg"> <a href="{{ route('posts.create') }}" class="hover:underline hover:underline-offset-1" >New Post</a > <a href="https://github.com/ericnanhu" class="hover:underline hover:underline-offset-1" >GitHub</a > </div> </nav> </div> @yield('content') <footer class="bg-gray-700 text-white"> <div class="flex justify-center items-center sm:justify-between flex-wrap lg:max-w-screen-2xl mx-auto px-4 sm:px-8 py-10" > <p class="font-serif text-center mb-3 sm:mb-0"> Copyright © <a href="https://www.ericsdevblog.com/" class="hover:underline" >Eric Hu</a > </p> <div class="flex justify-center space-x-4">. . .</div> </div> </footer> </div> </body> </html> لاحظ في السطرين 16 و 20 وجود الأقواس المزدوجة المتعرجة {{ }} التي تسمح بتنفيذ شيفرة PHP البرمجية ضمن القالب، وبالتالي سيعيد التابع route('posts.index')‎ الوِجهة التي اسمها posts.index، حيث يمكنك التحقق من أسماء الوِجهات من خلال الرجوع إلى خرج الأمر php artisan route:list. لننتقل بعد ذلك إلى العرض create، حيث يجب أن تكون منظمًا في هذا العرض. يُعَد العرض create مُخصّصًا للإجراءات المتعلقة بالمنشور، لذا أنشأنا المجلد post لتخزين هذا العرض. سيتضمن العرض views/posts/create.blade.php ما يلي: @extends('layout') @section('title') <title>Create</title> @endsection @section('content') <div class="w-96 mx-auto my-8"> <h2 class="text-2xl font-semibold underline mb-4">Create new post</h2> <form action="{{ route('posts.store') }}" method="POST"> {{ csrf_field() }} <label for="title">Title:</label><br /> <input type="text" id="title" name="title" class="p-2 w-full bg-gray-50 rounded border border-gray-300 focus:ring-3 focus:ring-blue-300" /><br /> <br /> <label for="content">Content:</label><br /> <textarea type="text" id="content" name="content" rows="15" class="p-2 w-full bg-gray-50 rounded border border-gray-300 focus:ring-3 focus:ring-blue-300" ></textarea ><br /> <br /> <button type="submit" class="font-sans text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-md text-sm w-full px-5 py-2.5 text-center" > Submit </button> </form> </div> @endsection توجد بعض الأشياء التي يجب ملاحظتها عند استخدام الاستمارات لنقل البيانات، وهي: السطر 6: تحدِّد سمة Attribute الإجراء action ما يحدث عند إرسال هذه الاستمارة، وتوجّه المتصفح في هذه الحالة لزيارة الوِجهة posts.store باستخدام تابع HTTP الذي هو POST. السطر 7 ‎{{ csrf_field() }}‎: يُعَد CSRF هجومًا ضارًا يستهدف تطبيقات الويب، وتوفر الدالة csrf_field()‎ الحماية من هذا النوع من الهجمات. اطلع على مقال تعرف على أمان مواقع الويب لمعرفة المزيد عن هجمات تزوير الطلبات عبر المواقع Cross-Site Request Forgery -أو CSRF اختصارًا. السمة name في السطر 12 و 20: يُربَط ما يدخله المستخدم بمتغير عند إرسال الاستمارة، وتحدّد السمة name اسم هذا المتغير، فمثلًا إذا كان name="title"‎، فسيُربَط دخل المستخدم بالمتغير title، ويمكن الوصول إلى قيمته باستخدام التعليمة ‎$request->input('title')‎، حيث سنرى كيفية عملها لاحقًا. السطر 27: يجب ضبط السمة type على القيمة submit حتى تعمل هذه الاستمارة. شغّل الآن خادم التطوير وانتقل إلى العنوان http://127.0.0.1:8000/posts/create حيث ستظهر الصفحة التالية: إذا نقرتَ على زر الإرسال "Submit"، فسيرسل المتصفح طلب POST إلى الخادم، وسيُنفَّذ التابع store()‎ هذه المرة، وسيحتوي طلب POST على دخل المستخدم، ويمكن الوصول إليه كما يلي في الملف PostController.php: <?php namespace App\Http\Controllers; . . . class PostController extends Controller { . . . /** * تخزين مورد أنشأناه حديثًا في وحدة التخزين */ public function store(Request $request): RedirectResponse { // الحصول على البيانات من الطلب $title = $request->input('title'); $content = $request->input('content'); // إنشاء نسخة جديدة من‫ المنشور Post ووضع البيانات المطلوبة في العمود المقابل $post = new Post; $post->title = $title; $post->content = $content; // حفظ البيانات $post->save(); return redirect()->route('posts.index'); } . . . } سيُعاد توجيهك إلى الوِجهة posts.index بعد تخزين البيانات. عُد إلى متصفحك واكتب عنوانًا ومحتوًى جديدًا، ثم انقر على زر الإرسال. لم ننشئ العرض index بعد، لذا ستُعاد رسالة خطأ، ولكن إذا تحققتَ من قاعدة البيانات، فيجب وجود سجل جديد مضاف كما يلي: إجراء القائمة List لننتقل الآن إلى عملية القراءة Read، حيث يوجد نوعان مختلفان من عمليات القراءة هما: الأول هو إجراء القائمة الذي يعيد قائمةً بجميع المنشورات إلى المستخدم، والإجراء الثاني سنوضّحه في الفقرة التالية. يقابل إجراء القائمة التابعَ index()‎ في الملف PostController.php كما يلي: <?php namespace App\Http\Controllers; use App\Models\Post; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; class PostController extends Controller { /** * عرض قائمة الموارد */ public function index(): View { $posts = Post::all(); return view('posts.index', [ 'posts' => $posts, ]); } . . . } ويكون عرض index المقابل هو views/post/index.blade.php كما يلي: @extends('layout') @section('title') <title>Page Title</title> @endsection @section('content') <div class="max-w-screen-lg mx-auto my-8"> @foreach($posts as $post) <h2 class="text-2xl font-semibold underline mb-2"> <a href="{{ route('posts.show', ['post' => $post->id]) }}" >{{ $post->title }}</a > </h2> <p class="mb-4">{{ Illuminate\Support\Str::words($post->content, 100) }}</p> @endforeach </div> @endsection تتكرر حلقة foreach في السطر 5 على جميع المنشورات ‎$posts المُسترَدة، ثم تسنِد كل قيمة إلى المتغير ‎$post. لاحظ في السطر7 كيفية تمرير معرّف id المنشور إلى الوِجهة posts.show، ثم ستمرِّر هذه الوِجهة هذا المتغير إلى تابع المتحكم show()‎ الذي سنراه لاحقًا. يُعَد التابع Str::words()‎ في السطر 11 دالةً مساعدة في لغة PHP، وسيأخذ أول 100 كلمة من المحتوى content فقط. إجراء العرض Show عملية القراءة الثانية هي إجراء العرض الذي يعرض تفاصيل مورد معين، ويتحقّق هذا الإجراء باستخدام التابع show()‎ في الملف PostController.php كما يلي: <?php namespace App\Http\Controllers; use App\Models\Post; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; class PostController extends Controller { . . . /** * عرض المورد المُحدَّد */ public function show(string $id): View { $post = Post::all()->find($id); return view('posts.show', [ 'post' => $post, ]); } . . . } ويكون عرض show المقابل هو views/post/show.blade.php كما يلي: @extends('layout') @section('title') <title>{{ $post->title }}</title> @endsection @section('content') <div class="max-w-screen-lg mx-auto my-8"> <h2 class="text-2xl font-semibold underline mb-2">{{ $post->title }}</h2> <p class="mb-4">{{ $post->content }}</p> <div class="grid grid-cols-2 gap-x-2"> <a href="{{ route('posts.edit', ['post' => $post->id]) }}" class="font-sans text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-md text-sm w-full px-5 py-2.5 text-center" >Update</a > <form action="{{ route('posts.destroy', ['post' => $post->id]) }}" method="POST" > {{ csrf_field() }} {{ method_field('DELETE') }} <button type="submit" class="font-sans text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-md text-sm w-full px-5 py-2.5 text-center" > Delete </button> </form> </div> </div> @endsection هناك بعض الأشياء التي يجب أن نلاحظها في المثال السابق. أولًا، لاحظ كيف أن زر التحديث "Update" هو رابط بسيط، ولكن زر الحذف "Delete" هو استمارة، لأن الرابط سيخبر المتصفح بإرسال طلب GET إلى الخادم، بينما نحتاج شيئًا آخر لإجراء الحذف، حيث إذا نظرتَ داخل الاستمارة، فستجد أنها تحتوي على السمة method="POST"‎، ولكننا نحتاج التابع DELETE لإجراء الحذف. تدعم لغة HTML التابعين GET و POST افتراضيًا، وإذا كنت بحاجة شيء آخر، فيجب أن تضبط السمة method="POST"‎، وأن تستخدم التابع method_field()‎ بدلًا من ذلك. إجراء التحديث Update تستخدم عملية التحديث التابع edit()‎ الذي يعرض استمارة HTML، والتابع update()‎ الذي يجري تغييرات على قاعدة البيانات، لذا ضع هذين التابعين في الملف PostController.php كما يلي: <?php namespace App\Http\Controllers; use App\Models\Post; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; class PostController extends Controller { . . . /** * عرض الاستمارة الخاصة بتعديل المورد المُحدَّد */ public function edit(string $id): View { $post = Post::all()->find($id); return view('posts.edit', [ 'post' => $post, ]); } /** * تحديث المورد المحدَّد في وحدة التخزين */ public function update(Request $request, string $id): RedirectResponse { // الحصول على البيانات من الطلب $title = $request->input('title'); $content = $request->input('content'); // البحث عن المنشور المطلوب ووضع البيانات المطلوبة في العمود المقابل $post = Post::all()->find($id); $post->title = $title; $post->content = $content; // حفظ البيانات $post->save(); return redirect()->route('posts.show', ['post' => $id]); } . . . } ويكون عرض edit المقابل هو views/post/edit.blade.php كما يلي: @extends('layout') @section('title') <title>Edit</title> @endsection @section('content') <div class="w-96 mx-auto my-8"> <h2 class="text-2xl font-semibold underline mb-4">Edit post</h2> <form action="{{ route('posts.update', ['post' => $post->id]) }}" method="POST" > {{ csrf_field() }} {{ method_field('PUT') }} <label for="title">Title:</label><br /> <input type="text" id="title" name="title" value="{{ $post->title }}" class="p-2 w-full bg-gray-50 rounded border border-gray-300 focus:ring-3 focus:ring-blue-300" /><br /> <br /> <label for="content">Content:</label><br /> <textarea type="text" id="content" name="content" rows="15" class="p-2 w-full bg-gray-50 rounded border border-gray-300 focus:ring-3 focus:ring-blue-300" > {{ $post->content }}</textarea ><br /> <br /> <button type="submit" class="font-sans text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-md text-sm w-full px-5 py-2.5 text-center" > Submit </button> </form> </div> @endsection إجراء الحذف Delete وأخيرًا، يوجد التابع destroy()‎ للحذف، إذًا لنضعه في الملف PostController.php كما يلي: <?php namespace App\Http\Controllers; use App\Models\Post; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; class PostController extends Controller { . . . /** * إزالة المورد المُحدَّد من وحدة التخزين */ public function destroy(string $id): RedirectResponse { $post = Post::all()->find($id); $post->delete(); return redirect()->route('posts.index'); } } لا يتطلب هذا الإجراء عرضًا، بما أنه يعيد توجيهك إلى posts.index بعد اكتمال الإجراء. ترجمة -وبتصرُّف- للمقال Laravel for Beginners #3 - The CRUD Operations لصاحبه Eric Hu. اقرأ أيضًا المقال السابق: لارافيل للمبتدئين-الجزء الثاني: استخدام بنية MVC لإنشاء مدونة بسيطة تعرف على إطار عمل تطوير الويب الشهير لارافيل Laravel المبادئ الأساسيّة لإطار العمل Eloquent ORM أساسيات التحقق من المدخلات في Laravel كيف تستخدم منشئ الاستعلامات Query builder للتخاطب مع قاعدة البيانات في Laravel
  17. تُعدّ لغة PHP من أكثر اللغات استخدامًا في تطوير مواقع الويب لأنها سهلة وتتيح بناء تطبيقات الويب بسرعة. لكن رغم سهولتها إلا أنها تطورت وأصبحت تضم العديد من المميزات والفروق الرئيسية والدقيقة التي يمكن أن يخطئ بها المطورون بالرغم من سهولة استخدامها، فقد يقع المطورون في أخطاء بسبب هذه التفاصيل الدقيقة في اللغة، مما يستغرق منهم وقتًا طويلًا لإصلاحها. في هذا المقال. سنسلط الضوء في هذا المقال على 10 من الأخطاء الأكثر شيوعًا التي يجب على مطوري PHP توخّي الحذر منها. الخطأ 1: ترك مراجع References المصفوفة معلّقة بعد انتهاء حلقات foreach يمكن أن يكون استخدام المراجع في حلقات foreach مفيدًا إذا أردتَ العمل على كل عنصر من المصفوفة التي تمر عليها، فليكن لدينا مثلًا ما يلي: $arr = array(1, 2, 3, 4); foreach ($arr as &$value) { $value = $value * 2; } // $arr is now array(2, 4, 6, 8) ولكن إن لم تكن حذرًا، فقد يكون لهذه المراجع بعض الآثار الجانبية والعواقب غير المرغوب فيها، فمثلًا سيبقى المتغير ‎$value ضمن النطاق Scope وسيحتفظ بمرجع إلى العنصر الأخير من المصفوفة بعد تنفيذ الشيفرة البرمجية في المثال السابق، وبالتالي قد تؤدي العمليات اللاحقة المُطبَّقة على المتغير ‎$value إلى تعديل العنصر الأخير في المصفوفة عن غير قصد. تذكّر أن حلقة foreach لا تنشئ نطاقًا، وبالتالي يُعَد المتغير ‎$value في المثال السابق مرجعًا ضمن النطاق العلوي للسكريبت، حيث تضبط حلقة foreach المرجع للإشارة إلى العنصر التالي من المصفوفة ‎$arr في كل تكرار، ويبقى المتغير ‎$value يؤشّر إلى العنصر الأخير من المصفوفة ‎$arr، ويبقى ضمن النطاق بعد اكتمال الحلقة. إليك فيما يلي مثال عن نوع الأخطاء المربكة التي يمكن أن تؤدي إلى هذه المشكلة: $array = [1, 2, 3]; echo implode(',', $array), "\n"; foreach ($array as &$value) {} // من خلال المرجع echo implode(',', $array), "\n"; foreach ($array as $value) {} // من خلال القيمة (أي النسخة‫) echo implode(',', $array), "\n"; وسيكون خرج الشيفرة البرمجية السابقة ما يلي: 1,2,3 1,2,3 1,2,2 هذا ليس خطأ مطبعيًا، فالقيمة الأخيرة في السطر الأخير هي 2 وليست 3، والسبب هو بقاء المصفوفة ‎$array دون تغيير بعد المرور على حلقة foreach الأولى، ولكن يُترَك المتغير ‎$value بوصفه مرجعًا مُعلَّقًا للعنصر الأخير في المصفوفة ‎$array لأن حلقة foreach وصلت إلى المتغير ‎$value من خلال المرجع، لذا ستحدث أشياء غريبة عندما نمر على حلقة foreach الثانية، حيث تنسخ حلقة foreach كل عنصر تسلسلي من عناصر المصفوفة ‎$array إلى المتغير ‎$value في كل خطوة من الحلقة بسبب الوصول إلى هذا المتغير من خلال القيمة (أي من خلال النسخة). إليك ما يحدث في كل خطوة من حلقة foreach الثانية: تمرير القيمة 1: يؤدي إلى نسخ العنصر الأول من المصفوفة ‎$array[0]‎ (أي القيمة "1") إلى المتغير ‎$value (وهو مرجع إلى العنصر ‎$array[2]‎)، لذا سيساوي ‎$array[2]‎ الآن القيمة 1، وستحتوي المصفوفة ‎$array على العناصر ‎[1, 2, 1]‎. تمرير القيمة 2: يؤدي إلى نسخ العنصر الثاني من المصفوفة ‎$array[1]‎ (أي القيمة "2") إلى المتغير ‎$value (وهو مرجع إلى العنصر ‎$array[2]‎)، لذا سيساوي ‎$array[2]‎ الآن القيمة 2، وستحتوي المصفوفة ‎$array على العناصر ‎[1, 2, 2]‎. تمرير القيمة 3: يؤدي إلى نسخ العنصر الثالث من المصفوفة ‎$array[2]‎ (الذي يساوي القيمة "2" الآن) إلى المتغير ‎$value (وهو مرجع إلى العنصر ‎$array[2]‎)، لذلك لا يزال العنصر ‎$array[2]‎ يساوي القيمة 2، وستحتوي المصفوفة ‎$array الآن على العناصر ‎[1, 2, 2]‎. يمكن الاستفادة من استخدام المراجع في حلقات foreach دون التعرض لخطر هذه الأنواع من المشاكل من خلال استدعاء الدالة unset()‎ مع المتغير بعد حلقة foreach مباشرةً لإزالة المرجع كما في المثال التالي: $arr = array(1, 2, 3, 4); foreach ($arr as &$value) { $value = $value * 2; } unset($value); // لم يَعُد المتغير‫ ‎$value مرجعًا إلى العنصر ‎$arr[3]‎ الخطأ 2: سوء فهم سلوك الدالة isset()‎ تعيد الدالة isset()‎ القيمة false في حالة عدم وجود عنصر، وتعيد أيضًا false للقيم null، حيث يُسبّب هذا السلوك مشكلات، فليكن لدينا المثال التالي: $data = fetchRecordFromStorage($storage, $identifier); if (!isset($data['keyShouldBeSet']) { // افعل شيئًا هنا عند عدم ضبط‫ 'keyShouldBeSet' } يُفترَض أننا نريد التحقق ممّا إذا كان keyShouldBeSet مضبوطًا في ‎$data، ولكن ستعيد الدالة isset($data['keyShouldBeSet'])‎ أيضًا القيمة false حتى عند ضبط ‎$data['keyShouldBeSet']‎ على القيمة null، وبالتالي يوجد تناقض مع ما ذكرناه سابقًا. إليك أيضًا المثال التالي: if ($_POST['active']) { $postData = extractSomething($_POST); } // ... if (!isset($postData)) { echo 'post not active'; } تفترض الشيفرة البرمجية السابقة أنه إذا أعادت ‎$_POST['active']‎ القيمة true، فسيكون postData مضبوطًا، وبالتالي ستعيد الدالة isset($postData)‎ القيمة true، وتفترض أيضًا أن الطريقة الوحيدة التي ستعيد بها الدالة isset($postData)‎ القيمة false هي إذا أعادت ‎$_POST['active']‎ القيمة false، ولكن ستعيد الدالة isset($postData)‎ القيمة false عند ضبط ‎$postData على القيمة null، وبالتالي يمكن أن تعيد الدالة isset($postData)‎ القيمة false حتى إذا أعادت ‎$_POST['active']‎ القيمة true، لذا يوجد تناقض مع ما ذكرناه سابقًا. إذا كان الهدف من الشيفرة البرمجية السابقة هو التحقق مما إذا كانت ‎$_POST['active']‎ تعيد القيمة true، فسيكون الاعتماد على الدالة isset()‎ لذلك قرارًا برمجيًا سيئًا على أيّة حال، لذا يُفضَّل إعادة التحقق من ‎$_POST['active']‎ بدلًا من ذلك كما يلي: if ($_POST['active']) { $postData = extractSomething($_POST); } // ... if ($_POST['active']) { echo 'post not active'; } يُعَد استخدام التابع ‎array_key_exists()‎ حلًا أكثر قوة بالنسبة للحالات التي يكون فيها من المهم التحقق مما إذا كان المتغير مضبوطًا فعليًا، أو للتمييز بين متغير غير مضبوط ومتغير مضبوط على القيمة null، فمثلًا يمكننا إعادة كتابة المثال الأول من المثالين السابقين كما يلي: $data = fetchRecordFromStorage($storage, $identifier); if (! array_key_exists('keyShouldBeSet', $data)) { // افعل ذلك عند عدم ضبط‫ 'keyShouldBeSet' } إذا جمعنا بين التابعين array_key_exists()‎ و get_defined_vars()‎، فيمكننا التحقق بطريقة موثوقة من ضبط متغير ضمن النطاق الحالي كما يلي: if (array_key_exists('varShouldBeSet', get_defined_vars())) { // ‫المتغير ‎$varShouldBeSet موجود في النطاق الحالي } الخطأ 3: الخلط بين الإعادة باستخدام المرجع وإعادتها باستخدام القيمة ليكن لدينا مقطع الشيفرة البرمجية التالي: class Config { private $values = []; public function getValues() { return $this->values; } } $config = new Config(); $config->getValues()['test'] = 'test'; echo $config->getValues()['test']; إذا شغّلتَ الشيفرة البرمجية السابقة، فستحصل على ما يلي: PHP Notice: Undefined index: test in /path/to/my/script.php on line 21 تكمن المشكلة في أن هذه الشيفرة البرمجية تخلط بين إعادة المصفوفات باستخدام المرجع وإعادتها باستخدام القيمة، حيث إن لم تخبر لغةَ PHP صراحةً بإعادة المصفوفة باستخدام المرجع (أي باستخدام &)، فستعيد لغة PHP المصفوفة باستخدام القيمة افتراضيًا، وهذا يمثّل إعادة نسخة من المصفوفة، وبالتالي لن تتمكّن الدالة المُستدعاة والمستدعِي من الوصول إلى النسخة نفسها من المصفوفة. يعيد استدعاء الدالة getValues()‎ السابق نسخة من المصفوفة ‎$values بدلًا من إعادة مرجع إليها. إذًا لنعيد النظر إلى السطرين الرئيسيين من المثال السابق: // تعيد الدالة‫ getValues()‎ نسخة من المصفوفة ‎$values، مما يؤدي إلى إضافة عنصر 'test' // إلى نسخة من المصفوفة‫ ‎$values، وليس إلى المصفوفة ‎$values نفسها $config->getValues()['test'] = 'test'; // تعيد الدالة‫ getValues()‎ مرة أخرى نسخة أخرى من المصفوفة ‎$values، ولا تحتوي هذه النسخة // ‫على عنصر 'test'، ولذلك نحصل على رسالة فهرس غير مُعرَّف "undefined index" echo $config->getValues()['test']; أحد الحلول الممكنة لهذه المشكلة هو حفظ النسخة الأولى من المصفوفة ‎$values التي تعيدها الدالة getValues()‎، ثم العمل على تلك النسخة لاحقًا كما في المثال التالي: $vals = $config->getValues(); $vals['test'] = 'test'; echo $vals['test']; ستعمل هذه الشيفرة البرمجية بنجاح، أي أنها ستعطي test دون إنشاء أيّ رسالة فهرس غير مُعرَّف "undefined index"، ولكن قد يكون هذا الأسلوب مناسبًا أو غير مناسب اعتمادًا على ما تحاول تحقيقه، إذ لن تعدّل هذه الشيفرة البرمجية المصفوفة ‎$values الأصلية، لذا إذا أردتَ أن تؤثر تعديلاتك (مثل إضافة عنصر test) على المصفوفة الأصلية، فيجب أن تعدّل الدالة getValues()‎ لإعادة مرجع إلى المصفوفة ‎$values نفسها. يمكن تحقيق ذلك من خلال إضافة & قبل اسم الدالة، مما يشير إلى أنها يجب أن تعيد مرجعًا كما يلي: class Config { private $values = []; // إعادة مرجع إلى المصفوفة‫ ‎$values الفعلية public function &getValues() { return $this->values; } } $config = new Config(); $config->getValues()['test'] = 'test'; echo $config->getValues()['test']; وسيكون الخرج test كما هو متوقع. لنجعل الآن الأمور أكثر إرباكًا كما في مقتطف الشيفرة البرمجية التالي: class Config { private $values; // استخدام كائن مصفوفة‫ ArrayObject بدلًا من المصفوفة public function __construct() { $this->values = new ArrayObject(); } public function getValues() { return $this->values; } } $config = new Config(); $config->getValues()['test'] = 'test'; echo $config->getValues()['test']; إذا اعتقدتَ أن هذه الشيفرة البرمجية ستؤدي إلى خطأ "undefined index" نفسه الموجود في مثال المصفوفة array السابق، فأنت مخطئ، إذ ستعمل هذه الشيفرة البرمجية بنجاح، والسبب هو أن لغة PHP تمرّر الكائنات من خلال المرجع دائمًا على عكس المصفوفات، فالكائن ArrayObject هو كائن من مكتبة PHP المعيارية -أو كائن SPL- يحاكي استخدام المصفوفات، ولكنه يعمل بوصفه كائنًا. ليس من الواضح دائمًا في لغة PHP ما إذا كنت تتعامل مع نسخة أو مرجع كما توضح هذه الأمثلة، لذلك يجب أن تفهم هذه السلوكيات الافتراضية مثل تمرير المتغيرات والمصفوفات من خلال القيمة وتمرير الكائنات من خلال المرجع، ويجب التحقق بعناية من توثيق واجهة برمجة التطبيقات API الخاصة بالدالة التي تستدعيها لمعرفة ما إذا كانت تعيد قيمة، أو نسخة من مصفوفة، أو مرجعًا إلى مصفوفة، أو مرجعًا إلى كائن. يجب أيضًا تجنب ممارسة إعادة مرجع إلى مصفوفة أو كائن ArrayObject، لأنه يوفر للمستدعِي القدرة على تعديل البيانات الخاصة للنسخة، وهذا يناقض مفهوم التغليف Encapsulation، لذا يُفضَّل استخدام النمط القديم للجوالب Getters والضوابط Setters كما في المثال التالي: class Config { private $values = []; public function setValue($key, $value) { $this->values[$key] = $value; } public function getValue($key) { return $this->values[$key]; } } $config = new Config(); $config->setValue('testKey', 'testValue'); echo $config->getValue('testKey'); // echos 'testValue' تمنح هذه الطريقة المستدعِي القدرةَ على ضبط أو جلب أيّ قيمة من المصفوفة دون توفير وصول عام إلى المصفوفة ‎$values الخاصة نفسها. الخطأ 4: إجراء الاستعلامات ضمن حلقة يمكن أن تصادف شيئًا كما يلي إذا كانت شيفرة PHP لا تعمل لديك: $models = []; foreach ($inputValues as $inputValue) { $models[] = $valueRepository->findByValue($inputValue); } قد لا يكون هناك أيّ شيء خاطئ، ولكن إذا اتبعت المنطق الموجود في هذه الشيفرة البرمجية، فقد تجد أن الاستدعاء البسيط السابق ‎$valueRepository->findByValue()‎ يؤدي في النهاية إلى استعلامٍ من نوعٍ ما كما يلي: $result = $connection->query("SELECT `x`,`y` FROM `values` WHERE `value`=" . $inputValue); لذا سيؤدي كل تكرار للحلقة السابقة إلى استعلام منفصل لقاعدة البيانات، فمثلًا إذا زوّدتَ الحلقة بمصفوفةٍ مكونة من 1000 قيمة، فستنشئ 1000 استعلام منفصل للمورد، وإذا اُستدعِي مثل هذا السكربت ضمن خيوط Threads متعددة، فيُحتمَل أن يؤدي ذلك إلى توقف النظام بأكمله، لذا يجب معرفة متى تُجري شيفرتك البرمجية الاستعلامات وجمع القيم ثم تشغيل استعلام واحد لجلب جميع النتائج كلما أمكن ذلك. أحد الأمثلة على الأماكن الشائعة إلى حدٍ ما التي ترى فيها إجراء الاستعلام بطريقة غير فعالة (أي ضمن حلقة) هو عند نشر استمارة مع قائمة من القيم (المعرّفات مثلًا)، ثم ستمر الشيفرة البرمجية ضمن حلقة على المصفوفة وتجري استعلام SQL منفصل لكل معرّف لاسترداد بيانات السجل الكاملة لكل معرّف من هذه المعرّفات كما يلي: $data = []; foreach ($ids as $id) { $result = $connection->query("SELECT `x`, `y` FROM `values` WHERE `id` = " . $id); $data[] = $result->fetch_row(); } ولكن يمكن تحقيق الشيء نفسه بفعالية أكبر في استعلام SQL واحد كما يلي: $data = []; if (count($ids)) { $result = $connection->query("SELECT `x`, `y` FROM `values` WHERE `id` IN (" . implode(',', $ids)); while ($row = $result->fetch_row()) { $data[] = $row; } } لذلك يجب أن تعرف متى تجري شيفرتك البرمجية الاستعلامات سواءً بطريقة مباشرة أو غير مباشرة، ويجب أن تجمع القيم ثم تشغّل استعلامًا واحدًا لجلب جميع النتائج كلما أمكنك ذلك، ولكن يجب توخي الحذر لكي لا نحصل على خطأ PHP الشائع التالي. الخطأ 5: تزييف استخدام الذاكرة وعدم كفاءتها يكون جلب العديد من السجلات في وقت واحد أكثر كفاءة من تشغيل استعلام واحد لجلب كل صف، ولكن يمكن أن يؤدي هذا النهج إلى حالة "نفاد الذاكرة Out of Memory" في مكتبة libmysqlclient عند استخدام إضافة PHP التي هي mysql. ليكن لدينا مثلًا مربع اختبار مع موارد محدودة (512 ميجابايت من ذاكرة RAM) وقاعدة بيانات MySQL وواجهة سطر الأوامر php-cli، حيث سنهيّئ جدول قاعدة بيانات كما يلي: // الاتصال بقاعدة بيانات‫ mysql $connection = new mysqli('localhost', 'username', 'password', 'database'); // إنشاء جدول مؤلّف من 400 عمود $query = 'CREATE TABLE `test`(`id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT'; for ($col = 0; $col < 400; $col++) { $query .= ", `col$col` CHAR(10) NOT NULL"; } $query .= ');'; $connection->query($query); // كتابة 2 مليون صف for ($row = 0; $row < 2000000; $row++) { $query = "INSERT INTO `test` VALUES ($row"; for ($col = 0; $col < 400; $col++) { $query .= ', ' . mt_rand(1000000000, 9999999999); } $query .= ')'; $connection->query($query); } لنتحقّق الآن من استخدام الموارد كما يلي: // الاتصال بقاعدة بيانات‫ mysql $connection = new mysqli('localhost', 'username', 'password', 'database'); echo "Before: " . memory_get_peak_usage() . "\n"; $res = $connection->query('SELECT `x`,`y` FROM `test` LIMIT 1'); echo "Limit 1: " . memory_get_peak_usage() . "\n"; $res = $connection->query('SELECT `x`,`y` FROM `test` LIMIT 10000'); echo "Limit 10000: " . memory_get_peak_usage() . "\n"; ويكون الخرج كما يلي: Before: 224704 Limit 1: 224704 Limit 10000: 224704 لاحظ أن الاستعلامات مُدارة داخليًا بأمان فيما يتعلق بالموارد، ولكن يمكن التأكد من ذلك من خلال زيادة الحد الأقصى مرة أخرى، حيث سنضبطه على القيمة 100000، وسينتج ما يلي: PHP Warning: mysqli::query(): (HY000/2013): Lost connection to MySQL server during query in /root/test.php on line 11 المشكلة هنا هي الطريقة التي تعمل بها وحدة mysql الخاصة بلغة PHP، فهي مجرد وكيل لمكتبة libmysqlclient، والتي تذهب إلى الذاكرة مباشرةً عند تحديد جزء من البيانات. لا يدير مدير PHP الذاكرة، وبالتالي لن تعرض الدالة memory_get_peak_usage()‎ أيّ زيادة في استخدام الموارد عندما نزيد الحد الأقصى في استعلامنا. يؤدي ذلك إلى ظهور مشاكل مثل المشكلة السابقة، إذ سنُخدَع من خلال الاعتقاد بأن إدارة ذاكرتنا جيدة، ولكنها سيئة فعليًا وسنواجه مثل هذه المشكلة. يمكنك تجنب هذا التزييف من خلال استخدام الوحدة mysqlnd بدلًا من ذلك، بالرغم من أنها لن تؤدي إلى تحسين استخدام الذاكرة، حيث تُصرَّف Compiled هذه الوحدة بوصفها إضافةً أصيلة Native للغة PHP وتستخدم مدير ذاكرة PHP، لذلك إذا أجرينا الاختبار السابق باستخدام الوحدة mysqlnd بدلًا من الوحدة mysql، فسنحصل على صورة أكثر واقعية لاستخدام ذاكرتنا. Before: 232048 Limit 1: 324952 Limit 10000: 32572912 تستخدم الوحدة mysql ضعف عدد الموارد التي تستخدمها الوحدة mysqlnd لتخزين البيانات وفقًا لتوثيق PHP، لذا يستخدم السكربت الأصلي الذي يستخدم وحدة mysql ذاكرةً أكبر مما هو موضّح هنا فعليًا (حوالي ضعف ذلك). يمكن تجنب مثل هذه المشاكل من خلال تحديد حجم استعلاماتك واستخدام حلقة ذات عدد صغير من التكرارات كما يلي: $totalNumberToFetch = 10000; $portionSize = 100; for ($i = 0; $i <= ceil($totalNumberToFetch / $portionSize); $i++) { $limitFrom = $portionSize * $i; $res = $connection->query( "SELECT `x`,`y` FROM `test` LIMIT $limitFrom, $portionSize"); } إذا أمعنا النظر في هذا الخطأ والخطأ رقم 4، فإننا سندرك أن هناك توازنًا صحيًا تحتاج شيفرتك البرمجية إلى تحقيقه بطريقة مثالية، حيث يجب إما أن تكون استعلاماتك صغيرة ومتكررة جدًا من ناحية، أو أن يكون كل استعلام فردي كبيرًا جدًا من ناحية أخرى، لذا يجب إيجاد توازن بينهما، إذ يمكن أن يسبّب استخدام أيٍّ منهما إلى الحد الأقصى مشكلات مع شيفرة PHP البرمجية التي لن تعمل بطريقة صحيحة. الخطأ 6: تجاهل مشكلات الترميز Unicode/UTF-8 تُعَد هذه المشكلة خاصة بلغة PHP بحد ذاتها أكثر من كونها مشكلة قد تظهر أثناء تنقيح أخطاء شيفرة PHP، ولكن هذه المشكلة لم تُعالَج بطريقة كافية، إذ كان من المقرر جعل نواة PHP 6 متوافقةً مع الترميز الموحّد Unicode، وجرى تعليق ذلك عند تعليق تطوير PHP 6 في عام 2010، ولكن ذلك لا يعفي المطور بأيّ حال من الأحوال من تقديم ترميز UTF-8 بطريقة صحيحة وتجنب الافتراض الخاطئ بأن جميع السلاسل النصية ستكون "نصًا عاديًا قديمًا يستخدم معيار ASCII". تشتهر الشيفرة البرمجية التي تفشل في معالجة السلاسل النصية التي ليست ASCII بإدخال أخطاء هايزنبغ Heisenbug المعقدة في شيفرتك البرمجية، إذ قد تسبّب حتى استدعاءات strlen($_POST['name'])‎ البسيطة مشكلات إذا حاول شخص يحمل اسم عائلة مثل "Schrödinger" التسجيل في نظامك. إليك فيما يلي قائمة صغيرة لتجنب مثل هذه المشكلات في شيفرتك البرمجية: إذا كنت لا تعرف الكثير عن ترميز Unicode و UTF-8، فيجب أن تتعلّم الأساسيات على الأقل. تأكد من استخدام دوال mb_*‎ دائمًا بدلًا من دوال السلاسل النصية القديمة، وتأكد من تضمين إضافة "تعدد البايتات multibyte" في إصدار PHP الخاص بك. تأكد من ضبط قاعدة البيانات والجداول الخاصة بك لاستخدام ترميز Unicode، إذ لا تزال العديد من إصدارات MySQL تستخدم معيار latin1 افتراضيًا. تذكّر أن الدالة json_encode()‎ تحوّل الرموز التي ليست رموز ASCII، حيث تحوّل السلسلة النصية "Schrödinger" إلى "Schr\u00f6dinger" مثلًا، ولكن التابع serialize()‎ لا يفعل ذلك. تأكّد من أن ملفات شيفرة PHP البرمجية الخاصة بك مشفَّرة أيضًا باستخدام ترميز UTF-8 لتجنب التعارض عند ضم السلاسل النصية مع ثوابت السلاسل النصية المكتوبة أو المضبوطة. اطّلع على مقال معالجة الملفات والبيانات المرمزة بترميز UTF-8 في PHP لمزيدٍ من المعلومات. الخطأ 7: افتراض أن المصفوفة ‎$_POST ستحتوي على بيانات POST الخاصة بك دائمًا لن تحتوي المصفوفة ‎$_POST على بيانات POST الخاصة بك دائمًا على عكس ما يدل عليه اسمها، إذ يمكن أن تكون فارغة. لنفترض مثلًا أننا أنشأنا طلب خادم باستخدام استدعاء التابع jQuery.ajax()‎ كما يلي: // js $.ajax({ url: 'http://my.site/some/path', method: 'post', data: JSON.stringify({a: 'a', b: 'b'}), contentType: 'application/json' }); لاحظ نوع المحتوى contentType: 'application/json'‎ هنا، حيث نرسل البيانات بتنسيق JSON، وهو أمر شائع جدًا لواجهات برمجة التطبيقات، وهو الإعداد الافتراضي للنشر في خدمة AngularJS التي هي ‎$http مثلًا. نفرّغ معلومات محتوى المصفوفة ‎$_POST من طرف الخادم في مثالنا كما يلي: // php var_dump($_POST); والمفاجأة أن النتيجة ستكون كما يلي: array(0) { } لم تظهر سلسلة JSON النصية الخاصة بنا ‎{a: 'a', b: 'b'}‎، حيث تحلّل لغة PHP حِمل طلب POST تلقائيًا عندما يكون نوع المحتوى application/x-www-form-urlencoded أو multipart/form-data فقط، إذ كان هذان النوعان من المحتوى هما النوعان الوحيدان المُستخدَمان منذ سنوات عند تنفيذ المصفوفة ‎$_POST الخاصة بلغة PHP، لذا لا تحمّل لغة PHP حِمل طلب POST تلقائيًا مع أيّ نوع محتوى آخر حتى الأنواع التي تحظى بشعبية كبيرة اليوم مثل application/json. تُعَد المصفوفة ‎$_POST ذات نطاق عام عالي Superglobal، لذا إذا عدّلناها مرة واحدة (ويُفضَّل أن يكون ذلك في وقت مبكر من السكربت)، فستكون القيمة المُعدَّلة (بما في ذلك حِمل طلب POST) قابلةً للإشارة إليها من شيفرتنا البرمجية. يُعَد ذلك أمرًا مهمًا، لأن أطر عمل PHP وجميع السكربتات المخصَّصة تقريبًا تستخدم المصفوفة ‎$_POST استخدامًا شائعًا لاستخراج بيانات الطلب وتحويلها. نحتاج إلى تحليل محتويات الطلب يدويًا (أي فك تشفير بيانات JSON) وتعديل المتغير ‎$_POST عند معالجة حِمل طلب POST مع نوع المحتوى application/json مثلًا كما يلي: // php $_POST = json_decode(file_get_contents('php://input'), true); إذا فرّغنا معلومات محتوى المصفوفة ‎$_POST بعد ذلك، فسنرى أنها تتضمّن حِمل طلب POST الصحيح كما في المثال التالي: array(2) { ["a"]=> string(1) "a" ["b"]=> string(1) "b" } الخطأ الشائع 8: الاعتقاد بأن لغة PHP تدعم نوع بيانات المحارف Character اطّلع على الشيفرة البرمجية التالية وحاول تخمين ما سينتج: for ($c = 'a'; $c <= 'z'; $c++) { echo $c . "\n"; } إذا أجبت أنها ستطبع المحارف من 'a' إلى 'z'، فقد تتفاجأ عندما تعرف أنك مخطئ، حيث أنها ستطبع المحارف من 'a' إلى 'z'، ولكنها ستطبع أيضًا 'aa' إلى 'yz'، إذ لا يوجد نوع البيانات char في لغة PHP، بينما يوجد نوع بيانات السلاسل النصية string فقط، وبالتالي سينتج عن زيادة السلسلة النصية z بمقدار واحد في لغة PHP السلسلة النصية aa: php> $c = 'z'; echo ++$c . "\n"; aa ولكن تُعَد السلسلة النصية aa أصغر من z من معجميًا: php> var_export((boolean)('aa' < 'z')) . "\n"; true لذلك تطبع الشيفرة البرمجية الحروف من a إلى z، ولكنها تطبع أيضًا بعد ذلك من aa إلى yz، وتتوقف عندما تصل إلى za، وهي القيمة الأولى الأكبر من z التي تصادفها: php> var_export((boolean)('za' < 'z')) . "\n"; false لذا استخدم الطريقة التالية للمرور ضمن حلقة على القيم من 'a' إلى 'z' في لغة PHP بصورة صحيحة: for ($i = ord('a'); $i <= ord('z'); $i++) { echo chr($i) . "\n"; } أو استخدم الطريقة التالية: $letters = range('a', 'z'); for ($i = 0; $i < count($letters); $i++) { echo $letters[$i] . "\n"; } الخطأ 9: تجاهل قواعد ومعايير كتابة الشيفرة البرمجية لا يؤدي تجاهل قواعد كتابة الشيفرة البرمجية مباشرةً إلى الحاجة إلى تنقيح أخطاء شيفرة PHP، ولكنه لا يزال أحد أهم الأشياء التي يجب مناقشتها، إذ قد يؤدي تجاهل هذه المعايير إلى حدوث عدد كبير من المشكلات في المشروع مثل الحصول على شيفرة برمجية غير متناسقة في أحسن الأحوال لأن كل مطور ينجز عمله بطريقته الخاصة، أو الحصول على شيفرة PHP لا تعمل أو من الصعب -أو من المستحيل في بعض الأحيان- التنقل ضمنها، مما يصعّب تنقيح أخطائها وتحسينها وصيانتها في أسوأ الأحوال. يؤدي ذلك إلى تخفيض إنتاجية فريقك بما في ذلك الكثير من الجهد الضائع أو غير الضروري. توجد توصية لمعايير PHP أو PHP Standards Recommendation -أو PSR اختصارًا، والتي تتألف من المعايير الخمسة التالية: PSR-0: معيار التحميل التلقائي. PSR-1: معيار كتابة الشيفرة البرمجية الأساسي. PSR-2: دليل تنسيق كتابة الشيفرة البرمجية. PSR-3: واجهة المسجِّل. PSR-4: المحمِّل التلقائي. لقد أُنشِئت معايير PSR بناءً على مدخلات من المشرفين على المنصات الأشهر في السوق لتوفر إرشادات وممارسات موحدة لتنظيم كتابة الأكواد في PHP. وقد وُضعت هذه المعايير من قبل مجموعة من المطورين والمشرفين على أبرز المنصات وأطر العمل الشهيرة مثل Zend و Drupal و Symfony و Joomla وغيرها في هذه المعايير، وتتبع هذه المنصات تلك المعايير، ويشارك الآن إطار عمل PEAR الذي حاول أن يكون معيارًا لسنوات قبل ذلك في وضع معايير PSR. لا يهم تقريبًا ما هو معيار كتابة الشيفرة البرمجية الخاص بك طالما أنك توافق على المعيار وتلتزم به، ولكن يُعَد اتباع معايير PSR فكرة جيدة إن لم يكن لديك سبب مقنع في مشروعك يخالفها، إذ أصبحت هناك العديد من الفرق والمشاريع التي تتوافق مع معايير PSR التي يُعِدّها أغلب مطوري PHP بوصفها المعيار الحالي، لذا سيساعد استخدامها في ضمان معرفة المطورين الجدد بمعيار كتابة الشيفرة البرمجية الخاص بك وسيشعرون بالارتياح عند انضمامهم إلى فريقك. الخطأ 10: استخدام الدالة empty()‎ استخدامًا خاطئًا يفضِّل بعض مطوري PHP استخدام الدالة empty()‎ لإجراء عمليات فحص منطقية لكل شيء تقريبًا، ولكن توجد حالات يمكن أن يؤدي فيها ذلك إلى الارتباك. أولًا، إذا عُدنا إلى المصفوفات ونُسخ كائن المصفوفة ArrayObject (الذي يحاكي المصفوفات)، فمن السهل افتراض أن المصفوفات ونسخ الكائن ArrayObject ستتصرف بطريقة مماثلة بسبب تشابههما، ولكنه افتراض خطير كما هو موضّح فيما يلي في الإصدار PHP 5.0: // ‫PHP 5.0 أو بعد‫: $array = []; var_dump(empty($array)); // ‫خرجها bool(true)‎ $array = new ArrayObject(); var_dump(empty($array)); // ‫خرجها bool(false)‎ // لماذا لا ينتج كلاهما الخرج نفسه؟ وستكون النتائج مختلفة في الإصدارات التي قبل الإصدار PHP 5.0: // الإصدارات التي قبل الإصدار‫ PHP 5.0: $array = []; var_dump(empty($array)); // ‫خرجها bool(false)‎ $array = new ArrayObject(); var_dump(empty($array)); // ‫خرجها bool(false)‎ تحظى هذه الطريقة بشعبية كبيرة لسوء الحظ، فمثلًا هذه هي الطريقة التي يعيد بها كائن Zend\Db\TableGateway في إطار عمل Zend Framework 2 البيانات عند استدعاء الدالة current()‎ مع نتيجة الدالة TableGateway::select()‎، ويمكن للمطور أن يصبح بسهولة ضحيةً لهذا الخطأ مع مثل هذه البيانات. يمكن تجنب هذه المشكلات من خلال التحقق من وجود بنى مصفوفات فارغة باستخدام الدالة count()‎ كما يلي: // ‫لاحظ أن ما يلي يعمل في جميع إصدارات PHP (سواء قبل أو بعد الإصدار 5.0): $array = []; var_dump(count($array)); // ‫خرجها int(0)‎ $array = new ArrayObject(); var_dump(count($array)); // ‫خرجها int(0)‎ تحوّل لغة PHP القيمة 0 إلى false، فيمكن أيضًا استخدام الدالة count()‎ ضمن شروط تعليمة if للتحقق من المصفوفات الفارغة. تجدر الإشارة أيضًا إلى أن الدالة ‎count()‎‎ في PHP لها تعقيد ثابت Constant Complexity (أو O(1)‎) على المصفوفات، مما يوضّح أنه الاختيار الصحيح. يوجد مثال آخر لذلك عندما تمثّل الدالة empty()‎ خطرًا من خلال دمجها مع دالة الأصناف السحرية ‎__get()‎. لنعرّف مثلًا صنفين Class مع وجود الخاصية test في كليهما، حيث نعرّف أولًا الصنف Regular الذي يتضمن الخاصية العادية test كما يلي: class Regular { public $test = 'value'; } نعرّف بعد ذلك الصنف Magic الذي يستخدم التابع السحري ‎__get()‎ للوصول إلى الخاصية test الخاصة به كما يلي: class Magic { private $values = ['test' => 'value']; public function __get($key) { if (isset($this->values[$key])) { return $this->values[$key]; } } } لنرى الآن ما سيحدث عندما نحاول الوصول إلى الخاصية test لكل صنف كما يلي: $regular = new Regular(); var_dump($regular->test); // ‫خرجها string(4) "value"‎ $magic = new Magic(); var_dump($magic->test); // ‫خرجها string(4) "value"‎ يبدو كل شيء على ما يرام حتى الآن، ولكن لنرى الآن ما يحدث عندما نستدعي الدالة empty()‎ مع كل منهما كما يلي: var_dump(empty($regular->test)); // ‫خرجها bool(false)‎ var_dump(empty($magic->test)); // خرجها‫ bool(true)‎ إذا اعتمدنا على الدالة empty()‎، فيمكن تضليلنا للاعتقاد بأن الخاصية test الخاصة بالصنف ‎$magic فارغة، بينما هي مضبوطة على القيمة 'value'. لسوء الحظ، إذا استخدم الصنفُ التابع السحري ‎__get()‎ لاسترداد قيمة خاصيةٍ ما، فلا توجد طريقة مضمونة للتحقق مما إذا كانت قيمة الخاصية فارغة أم لا. يمكنك خارج نطاق الصنف التحقق فقط من إعادة القيمة null، وهذا لا يعني بالضرورة عدم ضبط المفتاح المقابل، لأنه كان من الممكن ضبطه على القيمة null. إذا حاولنا الإشارة إلى خاصية غير موجودة لنسخة من الصنف Regular، فستظهر ملاحظة مشابهة لما يلي: Notice: Undefined property: Regular::$nonExistantTest in /path/to/test.php on line 10 Call Stack: 0.0012 234704 1. {main}() /path/to/test.php:0 لاحظ أنه يجب استخدام التابع empty()‎ بحذر لأنه قد يؤدي إلى نتائج مربكة ومضللة. الخلاصة يمكن أن تؤدي سهولة استخدام لغة PHP إلى جعل المطورين يشعرون بالراحة الزائدة في التعامل معها، مما قد يؤدي إلى تجاهل بعض الأخطاء الدقيقة التي تحتاج إلى تنقيح ومعالجة وعدم مراعاة بعض الفروق الدقيقة والخصوصيات في اللغة، ويمكن أن يؤدي ذلك إلى عدم عمل شيفرة PHP بنجاح وظهور إحدى المشكلات التي تعرضنا لها في هذا المقال. لقد تطورت لغة PHP تطورًا ملحوظًا على مدار تاريخها الطويل وشهدت تحسينات ومميزات عديدة، لذا يُعَد التعرف على تفاصيلها الدقيقة أمرًا جديرًا بالاهتمام، لأنها ستساعد على ضمان أن البرنامج الذي تنتجه أكثر قابلية للتوسع والقوة والصيانة. ترجمة -وبتصرُّف- للمقال Buggy PHP Code: The 10 Most Common Mistakes PHP Developers Make لصاحبه Ilya Sanosian. اقرأ أيضًا تعرف على لغة PHP تعلم لغة PHP اصطلاحات ومواضيع متفرقة مهمة لكل مبرمج PHP التعامل مع الأخطاء في PHP الأخطاء العشرة الأكثر شيوعًا في شيفرة بايثون Python البرمجية
  18. سنتحدث في هذا المقال عن معمارية MVC الخاصة بإطار عمل لارافيل Laravel، فبنية MVC هي مبدأ تصميم الويب الذي يتكون من النموذج ‎Model المسؤول عن التواصل مع قاعدة البيانات، والمتحكِّم ‎Controller وهو المكان الذي نخزّن فيه شيفرة تطبيقنا البرمجية، والعرض ‎View وهو الواجهة الأمامية للتطبيق. يوجد مُوجّه Router في تطبيق الويب عادةً، وهذا ما ناقشناه في المقال السابق، حيث يؤشّر الموجّه إلى المتحكم، ويبحث المتحكم عن البيانات المطلوبة في قاعدة البيانات عبر النموذج، ثم يضع البيانات المُسترَّدة إلى الموقع المقابل في العرض، وأخيرًا يعيد هذا العرض إلى المستخدم. طبقة العرض لنبدأ أولًا بطبقة بالعرض، حيث عرّفنا في المقال السابق الوِجهة Route التالية: Route::get('/', function () { return view('welcome'); }); تؤشّر هذه الوِجهة إلى العرض welcome المُخزَّن في المجلد resources/views وله الامتداد ‎.blade.php، حيث يُعلِم هذا الامتداد لارافيل أننا نستخدم نظام قوالب Blade الذي سنتحدث عنه لاحقًا. يمكننا إنشاء عرض آخر من خلال إنشاء ملف آخر له الامتداد نفسه في المجلد views، إذًا لننشئ العرض greetings.blade.php التالي: <html> <body> <h1>Hello!</h1> </body> </html> تحدثنا أيضًا في المقال السابق عن إمكانية تمرير البيانات من الوِجهة إلى العرض كما يلي: Route::get('/', function () { return view('greeting', ['name' => 'James']); }); أسندنا في المثال السابق القيمة 'James' إلى المتغير name، ومرّرنا هذا المتغير إلى العرض greeting.blade.php الذي أنشأناه مسبقًا، ويمكننا استخدام الأقواس المزدوجة المتعرجة {{ }} لعرض هذه البيانات في العرض greetings.blade.php كما يلي: <html> <body> <h1>Hello, {{ $name }}</h1> </body> </html> من الشائع تنظيم ملفات العرض في مجلدات مختلفة بالنسبة لتطبيقات الويب الكبيرة، فمثلًا يمكن أن يحتوي تطبيقك على لوحة إدارة، وبالتالي ستخزّن العروض Views المقابلة لها في مجلد فرعي اسمه admin مثلًا، وتذكّر استخدام . بدلًا من / عندما تؤشّر إلى ملفات العرض في بنية متداخلة من الموجّه، حيث يؤشّر المثال التالي إلى الملف home.blade.php الموجود في المجلد views/admin/‎: Route::get('/admin', function () { return view('admin.home'); }); صيغة قوالب Blade تستخدم عروض لارافيل قالب Blade افتراضيًا، وهو مشابه لنظام المكونات في إطار عمل Vue.js. تُعَد العروض مستندات HTML، ويضيف قالب Blade ميزات برمجية إليها مثل نظام الوراثة والتحكم في التدفق والحلقات والمفاهيم البرمجية الأخرى، مما يؤدي إلى إنشاء صفحة ويب أكثر ديناميكية مع كتابة تعليمات برمجية أقل. التعليمة الشرطية if يمكننا إنشاء تعليمات if باستخدام التوجيهات Directives التي هي ‎@if و ‎@elseif و ‎@else و ‎@endif، حيث تعمل هذه التعليمات مثل تعليمات if التي نراها في لغات البرمجة الأخرى. @if ($num == 1) <p>The number is one.</p> @elseif ($num == 2) <p>The number is two.</p> @else <p>The number is three.</p> @endif يمكننا اختبار هذه الشيفرة البرمجية باستخدام الوِجهة كما يلي: Route::get('/number/{num}', function ($num) { return view('view', ['num' => $num]); }); نفترض هنا أن المتغير num يمكن أن يكون قيمته 1 أو 2 أو 3 فقط. التعليمة switch تكون بنية هذه التعليمة كما يلي: @switch($i) @case(1) First case. . . @break @case(2) Second case. . . @break @default Default case. . . @endswitch الحلقات يمكن أن نمثل الحلقات بحلقة for بسيطة: @for ($i = 0; $i < 10; $i++) The current value is {{ $i }} @endfor أو بحلقة foreach، حيث يساوي المتغير ‎$user العنصر التالي من ‎$users لكل تكرار في المثال التالي: @foreach ($users as $user) <p>This is user {{ $user->id }}</p> @endforeach يمكن أن نمثّل الحلقات بحلقة while أيضًا: @while (true) <p>I'm looping forever.</p> @endwhile تُعَد هذه التعليمات بسيطة جدًا، وتعمل تمامًا مثل نظيراتها في لغة PHP، ولكن يوجد شيء خاص بهذه الحلقات هو المتغير ‎$loop، فمثلًا ليكن لدينا شيء نريد عرضه مرة واحدة فقط، حيث يمكنك تطبيق ما يلي في التكرار الأول: @foreach ($users as $user) @if ($loop->first) <p>This is the first iteration.</p> @endif <p>This is user {{ $user->id }}</p> @endforeach سيُعرَض عنصر الفقرة <p>This is the first iteration.</p> مرة واحدة فقط في التكرار الأول، وتوجد العديد من الخاصيات الأخرى التي يمكنك الوصول إليها، لذا اطلع عليها في توثيق لارافيل الرسمي. لاحظ أنه لا يمكن الوصول إلى المتغير ‎$loop إلا ضمن الحلقة. الصنف الشرطي Conditional Class يُعَد الصنف Class الطريقة الأكثر استخدامًا لإسناد التنسيقات Styles إلى عناصر HTML المختلفة، ويمكننا بسهولة تغيير مظهر هذا العنصر من خلال تغيير صنفه، حيث يقدّم لارافيل طريقة لإسناد الأصناف ديناميكيًا بالاعتماد على المتغيرات كما يلي: <span @class([ 'p-4', 'font-bold' => $isActive, 'bg-red' => $hasError, ])></span> يعتمد الصنفان font-bold و bg-red في المثال السابق على قيمة المتغيرين ‎$isActive و ‎$hasError‎. بناء تخطيط الصفحة Layout توجد دائمًا بعض أجزاء الصفحة التي ستظهر في صفحات متعددة عندما نبني تطبيق ويب، حيث تُعَد كتابة الشيفرة البرمجية نفسها مرارًا وتكرارًا مضيعة للوقت والموارد، ولا يُعَد ذلك جيدًا للصيانة، لذا يجب فصل الجزء الذي سيظهر عدة مرات ووضعه في ملف آخر، وسنستورده ببساطة عندما نحتاج إليه. يقدم إطار عمل لارافيل طريقتين مختلفتين لذلك هما: وراثة القوالب ووراثة المكونات، حيث يُعَد فهم وراثة القوالب أسهل بكثير للمبتدئين، ولكن توفّر المكونات مزيدًا من الميزات، إذًا لنبدأ أولًا بوراثة القوالب. وراثة القوالب نحاول في المثال التالي تعريف صفحة رئيسية، وتكون الوِجهة كما يلي: Route::get('/', function () { return view('home'); }); يستورد العرض layout.blade.php ملفات CSS وجافاسكريبت JavaScript الضرورية، بالإضافة إلى شريط التنقل وتذييل الصفحة التي ستظهر في جميع الصفحات، وسنرث layout.blade.php في الملف home.blade.php. لنضع أولًا ما يلي في الملف layout.blade.php: <html> <head> @yield('title') </head> <body> <div class="container">@yield('content')</div> </body> </html> ولنضع الآن ما يلي في الملف home.blade.php: @extends('layout') @section('title') <title>Home Page</title> @endsection @section('content') <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. . .</p> @endsection لاحظ أن ‎@section('title')‎ يقابل ‎@yield('title')‎، وأن ‎@section('content')‎ يقابل ‎@yield('content')‎. سيرى لارافيل أولًا التوجيه ‎@extends('layout')‎ عند عرض الصفحة الرئيسية في مثالنا، وسيعرف أنه يجب عليه الانتقال إلى الملف layout.blade.php. سيحدد لارافيل بعد ذلك موقع ‎@yield('title')‎ ويضع القسم title مكانه، ثم يبحث عن التوجيه ‎@yield('content')‎ ويضع القسم content مكانه. سنحتاج في بعض الأحيان إلى تحميل عرض آخر من العرض الحالي، فمثلًا إذا أردنا إضافة شريط جانبي إلى صفحتنا الرئيسية، وهو شيء سنضمّنه في بعض الصفحات دون صفحات أخرى، فلن نضعه في الملف layout.blade.php، ولكن سيكون من الصعب جدًا صيانته إذا أنشأتَ شريطًا جانبيًا لكل صفحة تتطلب ذلك. يمكننا في مثالنا إنشاء الملف sidebar.blade.php، ثم استخدام التوجيه ‎@include لاستيراد هذا الملف في الصفحة الرئيسية كما يلي: @extends('layout') @section('title') <title>Home Page</title> @endsection @section('content') <p>. . .</p> @include('sidebar') @endsection المكونات Components يشبه نظام المكونات في لارافيل إلى حد كبير النظام الموجود في إطار عمل Vue.js. سنبني صفحة رئيسية مع تخطيطها، ولكننا سنعرّف التخطيط بوصفه مكونًا، لذا سننشئ المجلد components ضمن المجلد resources/views/‎، ثم ننشئ الملف layout.blade.php بداخله. لنضع أولًا ما يلي في الملف views/components/layout.blade.php: <html> <head> . . . </head> <body> <div class="container">{{ $slot }}</div> </body> </html> يمكن استخدام هذا المكون من خلال استخدام الوسم <x-layout>، وتذكّر دائمًا البادئة x-‎. <x-layout> <p> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum eu elit semper ex varius vehicula. </p> </x-layout> سيُسنَد المحتوى الموجود ضمن العنصر <x-layout> إلى المتغير ‎$slot تلقائيًا، ولكن إذا كان لدينا فتحات Slots متعددة كما في مثالنا الذي يحتوي على قسم title وقسم content، فيمكن حل هذه المشكلة من خلال تعريف عنصر <x-slot> كما يلي: <x-layout> <x-slot name="title"> <title>Home Page</title> </x-slot> <p> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum eu elit semper ex varius vehicula. </p> </x-layout> سيُسنَد محتوى <x-slot name="title"‎> إلى المتغير ‎$title وسيُسنَد المحتوى المتبقي إلى المتغير ‎$slot كما يلي: <html> <head> {{ $title }} </head> <body> <div class="container">{{ $slot }}</div> </body> </html> التعامل مع قواعد البيانات في لارافيل سنناقش أولًا كيفية التعامل مع قواعد البيانات في لارافيل قبل أن نتحدث بمزيد من التفصيل عن طبقة النموذج والمتحكم في بنية MVC. يمكن أن نستخدم ملف ‎.txt بسيط لتخزين المعلومات عند استخدام جافاسكريبت JavaScript و Node.js، ولكن لن ينجح ذلك في تطبيق الويب الواقعي، لذا سنحتاج لاستخدام قاعدة بيانات لتخزين ومعالجتها المعلومات بكفاءة. استخدمنا في مقالنا الحالي أداة Sail الخاصة بلارافيل لإنشاء هذا المشروع، لذا أعدت هذه الأداة قاعدة بيانات MySQL، إذ يجب أن توجد متغيرات البيئة التالية في ملف ‎.env: DB_CONNECTION=mysql DB_HOST=mysql DB_PORT=3306 DB_DATABASE=curl_demo DB_USERNAME=sail DB_PASSWORD=password عمليات تهجير Migrations قاعدة البيانات تكون قاعدة البيانات فارغة حاليًا، لذا سنضيف بعض جداول قاعدة البيانات لاستخدامها، حيث ستحتوي هذه الجداول على أعمدة، وستكون للأعمدة أسماء وبعض المتطلبات الخاصة، وتسمى عملية الإعداد هذه بتشغيل عمليات التهجير. تُخزَّن جميع ملفات التهجير في لارافيل ضمن المجلد database/migrations، ويمكننا إنشاء ملف تهجير جديد من خلال استخدام الأمر البسيط التالي، ولكن إذا استخدمتَ أداة Sail لإنشاء مشروع لارافيل، فضع ‎./vendor/bin/sail بدلًا من php لتشغيل PHP داخل حاوية دوكر Docker، وإن لم تستخدم أداة Sail، فتابع كالمعتاد: php artisan make:migration create_flights_table يمكن تطبيق ملف التهجير كما يلي: php artisan migrate وإذا أردتَ التراجع عن عملية التهجير السابقة، فاستخدم الأمر التالي: php artisan migrate:rollback أو إذا أردتَ إعادة ضبط عمليات التهجير بالكامل والتراجع عن جميع التغييرات المطبقة على قاعدة البيانات، فاستخدم الأمر التالي: php artisan migrate:reset إنشاء جدول لنلقِ نظرة على المثال التالي قبل إنشاء ملف التهجير. افتح ملف التهجير database/migrations/2014_10_12_000000_create_users_table.php التالي الذي يأتي مع لارافيل: <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { /** * تشغيل عمليات التهجير */ public function up(): void { Schema::create('users', function (Blueprint $table) { $table->id(); $table->string('name'); $table->string('email')->unique(); $table->timestamp('email_verified_at')->nullable(); $table->string('password'); $table->rememberToken(); $table->timestamps(); }); } /** * عكس عمليات التهجير */ public function down(): void { Schema::dropIfExists('users'); } }; يحتوي هذا الملف على صنف class له تابعان مختلفان، حيث يُستخدَم التابع ‎up()‎ لإنشاء جداول وأعمدة جديدة، بينما يمكن استخدام التابع ‎down()‎ لعكس العمليات التي يطبّقها التابع ‎up()‎. يستخدم لارافيل داخل التابع ‎up()‎ التابع create()‎ ضمن الصنف Schema لإنشاء جدول جديد بالاسم 'users'. وينشئ كل سطر من السطور 15 إلى 21 في الشيفرة البرمجية السابقة عمودًا مختلفًا مع اسم ونوع مختلفين، فمثلًا ينشئ السطر ‎$table->string('name');‎ عمودًا بالاسم name ضمن الجدول users، ويجب أن يخزّن هذا العمود سلاسلًا نصية. اطّلع على القائمة الكاملة لأنواع الأعمدة المتوفرة في لارافيل. لن نستعرض جميع هذه الأنواع، بل سنناقش أهمها وسبب اختيارنا لنوع العمود عندما نواجه مشكلات محددة مستقبلًا. لاحظ السطر 17 في الشيفرة البرمجية السابقة ‎$table->string('email')->unique();‎، حيث يوجد التابع unique()‎ بعد التصريح عن اسم العمود ونوعه، حيث يُسمَّى هذا التابع بمُعدِّل العمود Column Modifier، لأنه يضيف قيودًا إضافية إلى هذا العمود، ويتأكد في هذه الحالة من أن العمود لا يمكن أن يحتوي على قيم متكررة، ويجب أن يكون كل بريد إلكتروني يقدّمه المستخدم فريدًا. اطّلع على القائمة الكاملة لمُعدِّلي الأعمدة المتوفرة في لارافيل. إجراء تغييرات على الجدول يمكن أيضًا تحديث الجداول باستخدام التابع table()‎ كما يلي، حيث تضيف الشيفرة البرمجية التالية عمودًا جديدًا هو العمود age إلى الجدول users الموجود مسبقًا: Schema::table('users', function (Blueprint $table) { $table->integer('age'); }); كما يمكننا إعادة تسمية الجدول باستخدام التابع rename()‎: Schema::rename($from, $to); ويمكننا حذف جدول نهائيًا كما يلي: Schema::drop('users'); Schema::dropIfExists('users'); البذر أو توليد البيانات Seeding يُستخدَم مولّد البيانات Seeder لإنشاء بيانات وهمية لقاعدة بياناتك، وبالتالي يسهّل عليك إجراء الاختبارات، حيث يمكنك إنشاء مولّد بيانات من خلال تشغيل الأمر التالي: php artisan make:seeder UserSeeder يولّد الأمر السابق مولّد بيانات للجدول users، وسيُوضَع في المجلد database/seeders، حيث وضع ما يلي في الملف UserSeeder.php: <?php namespace Database\Seeders; use Illuminate\Database\Seeder; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; class UserSeeder extends Seeder { /** * تشغيل مولّدات بيانات قاعدة البيانات */ public function run(): void { DB::table('users')->insert([ 'name' => Str::random(10), 'email' => Str::random(10).'@gmail.com', 'password' => Hash::make('password'), ]); } } شغّل الأمر التالي لتنفيذ مولّد البيانات السابق وملء جداول قاعدة البيانات بالبيانات الافتراضية: php artisan db:seed UserSeeder باني الاستعلامات Query Builder باني الاستعلامات هو واجهة تسمح لنا بالتفاعل مع قاعدة البيانات، فمثلًا يمكننا استخدام التابع table()‎ الذي يوفره الصنف DB لاسترداد جميع الصفوف الموجودة في الجدول كما يلي: use Illuminate\Support\Facades\DB; $users = DB::table('users')->get(); يمكننا إضافة مزيدٍ من القيود من خلال إضافة التابع where()‎ إلى سلسلة التوابع كما يلي، حيث ستحصل الشيفرة البرمجية التالية على جميع الصفوف التي تكون فيها القيمة المخزنة في العمود age أكبر من أو تساوي 21: $users = DB::table('users')->where('age', '>=', 21)->get(); توجد الكثير من التوابع الأخرى إلى جانب التابعين where()‎ و get()‎، والتي سنتحدث عنها لاحقًا. طبقة النموذج Model يُعَد النموذج مفهومًا مهمًا جدًا في تصميم الويب الحديث، فهو مسؤول عن التفاعل مع قاعدة بيانات تطبيق الويب، وهو جزء من نظام رابط الكائنات بالعلاقات Object-Relational Mapper -أو ORM اختصارًا- في لارافيل الذي هو نظام Eloquent، حيث يمكن عَدّه باني استعلام مع بعض الميزات الإضافية. يمكننا استخدام الأمر make:model لإنشاء نموذج جديد كما يلي: php artisan make:model Post إذا أردتَ توليد ملف تهجير مقابل للنموذج، فاستخدم الخيار ‎--migration: php artisan make:model Post --migration وسيتضمّن ملف النموذج app/Models/Post.php ما يلي: <?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Post extends Model { use HasFactory; . . . } يفترِض النموذج السابق وجود الجدول posts في قاعدة البيانات افتراضيًا، وإذا أردتَ تغيير هذا الإعداد، فاضبط الخاصية ‎$table كما يلي: class Post extends Model { /** * الجدول المرتبط بالنموذج * * @var string */ protected $table = 'my_posts'; } استرداد النموذج يُعَد نموذج Eloquent بانيَ استعلامات قويًا ويسمح بالتواصل مع قاعدة البيانات، فمثلًا يمكننا استخدام التابع all()‎ لاسترداد كافة السجلات في الجدول كما يلي، ولاحظ أننا استوردنا النموذج Post بدلًا من الصنف DB، حيث سيتصل النموذج Post تلقائيًا بالجدول posts: use App\Models\Post; $posts = Post::all(); بما أن النموذج هو باني استعلامات، لذا يمكن أن تصل النماذج إلى جميع توابع باني الاستعلامات كما يلي: $posts = Post::where('published', true) ->orderBy('title') ->take(10) ->get(); إذا استخدمتَ التابعين all()‎ أو get()‎ لاسترداد البيانات، فلن تكون القيمة المُعادة مصفوفةً أو كائنًا بسيطًا، ولكنها ستكون نسخة من الصنف Illuminate\Database\Eloquent\Collection، حيث يوفّر الصنف Collection عدة توابع أقوى من الكائنات البسيطة. يمكن استخدام التابع find()‎ مثلًا لتحديد موقع سجل باستخدام مفتاح رئيسي هو id عادة. $posts = Post::all(); $post = $posts->find(1); سيكون ‎$post في هذا المثال هو المنشور ذو المعرّف id==1. إدراج وتحديث النموذج يمكننا أيضًا إدراج السجلات أو تحديثها باستخدام النموذج، ولكن تأكد أولًا من أن النموذج المقابل له الخاصية ‎$fillable وتأكد من سرد جميع الأعمدة التي يجب أن تكون قابلة للملء. class Post extends Model { /** * السمات‫ Attributes التي يمكن إسنادها بطريقة جماعية * * @var array */ protected $fillable = ['title', 'content']; } يمكننا بعد ذلك استخدام التابع create()‎ لإنشاء سجل جديد كما يلي: $flight = Post::create([ 'title' => '. . .', 'content' => '. . .', ]); أو يمكننا تحديث السجلات الموجودة مسبقًا باستخدام التابع update()‎: Flight::where('published', true)->update(['published' => false]); حذف النموذج يوجد تابعان يسمحان بحذف السجلات، حيث إذا أردتَ حذف سجل واحد، فاستخدم التابع delete()‎: $posts = Post::all(); $post = $posts->find(1); $post->delete(); وإذا أردتَ حذف السجلات بطريقة جماعية، فاستخدم التابع truncate()‎: $posts = Post::where('published', true) ->orderBy('title') ->take(10) ->get(); $posts->truncate(); علاقات قاعدة البيانات لا تكون جداول قاعدة البيانات في تطبيقات الويب مستقلة بل تكون مترابطة بعلاقات فيما بينها، فمثلًا يمكن أن يكون لدينا مستخدم لديه منشورات ويمكن أن يكون لدينا منشوراتٌ تخص مستخدمًا ما، لذا يقدّم لارافيل طريقة لتعريف هذه العلاقات باستخدام نماذج Eloquent. يُحتمَل أن يكون القسم التالي صعبًا بعض الشيء بالنسبة للمبتدئين، ولكن لا تقلق بهذا الشأن، إذ سنعود إلى هذا الموضوع لاحقًا عندما نبدأ في إنشاء تطبيق المدونة، حيث سنوضّح فقط ما نحتاج إلى استخدامه لبناء نظام تدوين بسيط. علاقة واحد إلى واحد تُعَد علاقة واحد إلى واحد One to One العلاقة الأساسية، فمثلًا يرتبط كل مستخدم User بحيوان أليف Pet واحد. يمكن تعريف هذه العلاقة من خلال وضع التابع pet في النموذج User كما يلي: <?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class User extends Model { /** * الحصول على الحيوان الأليف المرتبط بالمستخدم */ public function pet() { return $this->hasOne(Pet::class); } } إذا أردنا أن نطبّق العكس، حيث يكون معكوس العلاقة "يملك" has one هو "يعود إلى" belongs to one، مما يعني أن كل حيوان أليف Pet يعود إلى مستخدم User واحد، ويمكن تعريف معكوس العلاقة واحد إلى واحد من خلال وضع التابع user في النموذج Pet كما يلي: class Pet extends Model { /** * الحصول على المستخدم الذي يملك الحيوان الأليف */ public function user() { return $this->belongsTo(User::class); } } لكننا لم ننتهِ بعد، إذ يجب أيضًا إجراء بعض التغييرات على جداول قاعدة البيانات المتقابلة، إذ يفترض النموذج User وجود العمود pet_id ضمن الجدول users، ويخزّن معرّف id الحيوان الأليف الذي يملكه المستخدم. يجب أيضًا إجراء تعديلات على الجدول pets المقابل، إذ يجب وجود العمود user_id الذي يخزّن معرّف id المستخدم الذي يعود إليه هذا الحيوان الأليف. علاقة واحد إلى متعدد تُستخدم علاقة واحد إلى متعدد One-to-Many لتعريف العلاقات التي يمتلك فيها نموذج واحد نسخًا متعددة من نموذج آخر، فمثلًا يمكن أن تحتوي فئة Category واحدة على العديد من المنشورات posts، ويمكن تعريف هذه العلاقة من خلال وضع التابع posts في النموذج Category كما يلي: class Category extends Model { public function posts() { return $this->hasMany(Post::class); } } ولكن يجب في بعض الأحيان العثور على الفئة عبر المنشور، فمعكوس العلاقة "لديه العديد" has many هو "ينتمي إلى" belongs to، ويمكن تعريف معكوس العلاقة واحد إلى متعدد من خلال وضع التابع category في النموذج Post كما يلي: class Post extends Model { public function category() { return $this->belongsTo(Category::class); } } تفترض هذه العلاقة أن الجدول posts يحتوي على العمود category_id الذي يخزّن معرّف id الفئة التي ينتمي إليها هذا المنشور. علاقة متعدد إلى متعدد تُعَد العلاقة متعدد إلى متعدد Many-to-Many أصعب بعض الشيء، فمثلًا يمكن أن يكون لدينا مستخدم User لديه العديد من الأدوار، ويمكن أن يكون لدينا دور Role له العديد من المستخدمين كما يلي: class User extends Model { /** * الأدوار التي تخص المستخدم */ public function roles() { return $this->belongsToMany(Role::class); } } class Role extends Model { /** * المستخدمون الذين ينتمون إلى الدور */ public function users() { return $this->belongsToMany(User::class); } } تفترض هذه العلاقة وجود الجدول role_user في قاعدة البيانات واحتواء الجدول role_user على العمودين user_id و role_id، وبالتالي يمكننا مطابقة المستخدم مع دوره والعكس صحيح. لنفترض أن لدينا الجدول role_user كما يلي: user_id role_id 1 1 2 1 3 2 1 2 2 3 يمتلك المستخدم الذي له المعرّف id=1 دورَين لهما المعرّف id=1 والمعرّف id=2. إذا أردنا عكس الأمر للعثور على المستخدمين عبر الدور، فيمكننا أن نرى مستخدمَين لهما المعرّف id=3 والمعرّف id=1 بالنسبة للدور ذي المعرّف id=2. طبقة المتحكم Controller وضّحنا الوِجهات والعروض والنماذج وعلاقات قاعدة البيانات، وحان الوقت الآن لنتعرّف على الشيء الذي يربطها جميعًا مع بعضها البعض. تحدّثنا في المقال السابق عن الوِجهات، واستخدمنا المثال التالي: Route::get('/number/{num}', function ($num) { return view('view', ['num' => $num]); }); يبدو الأمر جيدًا في الأمثلة البسيطة، ولكن إذا كان لديك مشروع ضخم، فسيؤدي وضع كل الشيفرة البرمجية في ملف الوِجهة Route File إلى جعل الأمر فوضويًا جدًا، والحل الأفضل هو التأكّد من أن الوِجهة تؤشّر دائمًا إلى تابع في المتحكم مع وضع الشيفرة البرمجية ضمن هذا التابع. use App\Http\Controllers\UserController; Route::get('/users/{name}', [UserController::class, 'show']); يمكن إنشاء متحكم جديد من خلال استخدام الأمر التالي: php artisan make:controller UserController تُخزَّن ملفات متحكم لارافيل ضمن المجلد app/Http/Controllers/‎. المتحكم الأساسي لنلقِ نظرة على المثال التالي، حيث يتوقع التابع show()‎ المتغير ‎$id، ويستخدِم هذا المعرّف id لتحديد موقع المستخدم في قاعدة البيانات، ويعيد هذا المستخدم في العرض. <?php namespace App\Http\Controllers; use App\Models\User; use Illuminate\View\View; class UserController extends Controller { /** * عرض الملف الشخصي لمستخدمٍ معين */ public function show(string $name): View { return view('user.profile', [ 'user' => User::firstWhere('name', $name); ]); } } تحتاج في بعض الأحيان إلى تعريف متحكم له تابع واحد فقط، ويمكننا في هذه الحالة تبسيط الشيفرة البرمجية من خلال تغيير اسم التابع إلى ‎_invoke. class UserController extends Controller { public function __invoke(string $name): View { return view('user.profile', [ 'user' => User::firstWhere('name', $name); ]); } } لا حاجة الآن لتحديد اسم التابع في الوِجهة كما يلي: Route::get('/users/{id}', UserController::class); متحكم الموارد المورد Resource هو مفهوم حديث آخر لتصميم الويب، حيث نرى جميع نماذج Eloquent بوصفها مواردًا، مما يعني أننا سنجري مجموعة الإجراءات نفسها عليها جميعًا، وهذه الإجراءات هي الإنشاء ‎Create والقراءة ‎Read والتحديث ‎Update والحذف ‎Delete -أو CRUD اختصارًا. يمكن إنشاء متحكم موارد من خلال استخدام الخيار ‎--resource كما يلي: php artisan make:controller PostController --resource سيحتوي المتحكم الذي أنشأناه على التوابع التالية تلقائيًا: <?php namespace App\Http\Controllers; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; class PostController extends Controller { /** * عرض قائمة الموارد */ public function index(): Response { // } /** * عرض الاستمارة الخاصة بإنشاء مورد جديد */ public function create(): Response { // } /** * تخزين المورد الذي أنشأناه حديثًا في وحدة التخزين */ public function store(Request $request): RedirectResponse { // } /** * عرض المورد المُحدَّد */ public function show(string $id): Response { // } /** * عرض الاستمارة الخاصة بتعديل المورد المُحدَّد */ public function edit(string $id): Response { // } /** * تحديث المورد المحدَّد في وحدة التخزين */ public function update(Request $request, string $id): RedirectResponse { // } /** * إزالة المورد المُحدَّد من وحدة التخزين */ public function destroy(string $id): RedirectResponse { // } } تشرح التعليقات في الشيفرة البرمجية السابقة وظيفة التوابع والغرض منها. يقدّم لارافيل أيضًا طريقة سهلة جدًا لتسجيل الوِجهات لكل من التوابع السابقة كما يلي: Route::resource('posts', PostController::class); ستَنشَأ الوِجهات التالية باستخدام توابع HTTP المختلفة تلقائيًا: تابع HTTP مسار URI الإجراء اسم الوِجهة GET ‏ ‎/posts ‏ index ‏ posts.index GET ‏ ‎/posts/create ‏ create ‏ posts.create POST ‏ ‎/posts ‏ store ‏ posts.store GET ‏ ‎/posts/{post}‎ ‏ show ‏ posts.show GET ‏ ‎/posts/{post}/edit ‏ edit ‏ posts.edit PUT/PATCH ‏ ‎/posts/{post}‎ ‏ update ‏ posts.update DELETE ‏ ‎/posts/{post}‎ ‏ destroy ‏ posts.destroy ترجمة -وبتصرُّف- للمقال Laravel for Beginners #2 - The MVC Structure لصاحبه Eric Hu. اقرأ أيضًا المقال السابق: لارافيل للمبتدئين-الجزء الأول: البدء في إنشاء مدونة بسيطة كيف تستخدم منشئ الاستعلامات Query builder للتخاطب مع قاعدة البيانات في Laravel تعرف على إطار عمل تطوير الويب لارافيل Laravel تجريد إعداد قواعد البيانات في لارافيل باستعمال عملية التهجير Migration والبذر Seeder علاقات Eloquent والتحميل الحثيث في Laravel 5
  19. يُعَد الجداء الشعاعي عمليةً تستخدم شعاعين على أساس معامَلين. وتكمن أهمية هذا في الرسوميات الحاسوبية عند محاولة العثور على شعاع عمودي على مستوٍ؛ ويُعَد ذلك ضروريًا عند حساب كيفية انعكاس الضوء على السطوح. سنوضّح في هذا المقال المواضيع التالية: تعريف الجداء الشعاعي لشعاعين. قاعدة اليد اليمنى: يكون منحى Orientation الجداء الشعاعي ‎u × v‎ باتجاه "الإبهام" إذا طويت أصابعك من الشعاع u إلى الشعاع v. خاصيات الجداء الشعاعي والتي هي: حجم Magnitude الجداء الشعاعي: حجم الجداء الشعاعي u × v هو مساحة متوازي الأضلاع التي يحدّدها هذان الشعاعان. لا يُعَد الجداء الشعاعي عملية تبديلية Anti-commutative: u × v = -(v × u)‎‎ u × u = 0‎ و (ku) × u = 0 نتيجة الجداء الشعاعي لشعاع مع شعاع صفري هي شعاع صفري: u × 0 = 0 × u = 0‎ لا يُعَد الجداء الشعاعي عملية تجميعية: ‎(u × v) × w ≠ u × (v × w) إمكانية توزيع الجداء الشعاعي على جمع الأشعة: u × (v + w) = u × v + u × w‎ الجداء الشعاعي بين محاور الإحداثيات: i × j = k و j × k = i و k × i = j حساب الجداء الشعاعي باستخدام المصفوفات العمودية. استخدام أداة مساعدة شبيهة بمحدّدات المصفوفات Determinant لتذكّر صيغة حساب الجداء الشعاعي للمصفوفات العمودية. تذكير: يكون نوع الكائنات التي تمثّلها معامَلات الجداء النقطي أشعةً، وتكون نتيجة الجداء النقطي عددًا حقيقيًا (مقدارًا سلميًا) Scalar. لقد شاهدت حتى الآن العمليات التالية: ولكن لا توجد عملية تشبه الضرب وتأخذ شعاعين كمعامَلين وينتج عنها شعاع، وهذا ما يمثله جداء الأشعة الشعاعي الذي سنشرحه في هذا المقال. تعريف الجداء الشعاعي إذا كان u و v شعاعين في فضاء ثلاثي الأبعاد، فإن الجداء الشعاعي لهما ‎u × v‎ هو شعاع ثلاثي الأبعاد، ويجب أن تكون معاملات هذه العملية ونتيجتها أشعةً ثلاثية الأبعاد على عكس العمليات الأخرى؛ إذ أن عملية الجداء الشعاعي ليست مُعرَّفةً في الفضاء ثنائي الأبعاد 2D. يمكن تعريف الجداء الشعاعي لشعاعين u × v كما يلي: يجب أن يكون u و v شعاعين ثلاثي الأبعاد. والنتيجة هي شعاع ثلاثي الأبعاد مع الطول والمنحى التاليين: الطول: ‎|‎ u × v | = | u | | v | sin θ حيث θ هي الزاوية بين الشعاعين u و v. المنحى: يكون ناتج الجداء الشعاعي u × v عموديًا على كلٍّ من الشعاعي u و v. يجري الاختيار من بين منحيين متعامدين على الشعاعين u و v باستخدام قاعدة اليد اليمنى. ناتج الجداء الشعاعي لشعاعين هو شعاع عمودي على هذه الشعاعين، ولكن هناك اتجاهين متعامدين على كلٍّ من هذين المعامَلين، حيث يمكن الاختيار من بينهما باستخدام قاعدة اليد اليمنى، والتي سنشرحها في الفقرة التالية. قاعدة اليد اليمنى Right-hand Rule نضع البرغي الخشبي عموديًا على لوحٍ من الخشب، ونلفّه باتجاه اللوح الخشبي باستخدام مفك براغي؛ إذ يجب أن ندير البرغي اليميني (النوع العادي) باتجاه عقارب الساعة من رأسه إلى اللوح الخشب. يكون الجداء الشعاعي لشعاعين متعامدًا عليهما، وتختار قاعدة اليد اليمنى أحد الاتجاهين المتعامدين المُحتمَلين. ويوضح الرسمان البيانيان السابقان هذه القاعدة. لا تنسَ أن ترتيب المعاملات مهم، إذ تؤشّر نتيجة الجداء الشعاعي ‎u × v‎ إلى الاتجاه نفسه الذي يجب أن يؤشّر إليه البرغي، إذا دفع الالتفافُ من الشعاع u إلى الشعاع v البرغي إلى داخل قطعة الخشب، أو فكّر في الاتجاه الذي يؤشّر إليه إبهامك عندما تطوي أصابعك من الشعاع u إلى الشعاع v. ملاحظة: يؤشّر الجداء الشعاعي ‎u × v‎ و ‎v × u‎ إلى اتجاهين متعاكسين. تصور الجداء الشعاعي يُظهر الشكل التالي الشعاعين u و v ومتوازي الأضلاع المُحدَّد بينهما. مساحة متوازي الأضلاع هي: ‎| ‎u | | v | sin θ، حيث θ هي الزاوية بينهما. تمثّل هذه المساحة حجم الجداء الشعاعي، وتكون هذه الحقيقة مفيدةً أحيانًا لتصور الجداء الشعاعي، فإذا دوّرنا الشعاع u مثلًا، بحيث يقترب منحاه من منحى الشعاع v (دون تغيير طوله)، فستقترب مساحة متوازي الأضلاع وحجم الجداء الشعاعي من الصفر، بينما تعطي الزاوية 90 درجة بين الشعاعين u و v القيمة العظمى للجداء الشعاعي. عكس ترتيب معاملات الجداء الشعاعي تذكير: يمكن عكس اتجاه الشعاع w من خلال ضربه بالعدد ‎-1؛ إذ يؤشّر الشعاعان w و ‎-w إلى اتجاهين متعاكسين. يوضح الشكل السابق حاصل عمليتي جداء شعاعين حدّدنا منحاهما باستخدام قاعدة اليد اليمنى، ويكون حجم الجداء الشعاعي: ‎‎u × v‎ = ‎| u | | v | sin θ = | v | | u | sin θ‎‎ وبالتالي: ‎u × v‎ = -(v × u)‎‎ ويمكن القول أن: ‎-(v × u) = -v × u‎‎‎ المعاملات المتوازية للجداء الشعاعي طول ‎u × v‎ هو ‎| u | | v | sin θ، وإذا كان u و v شعاعين على استقامة واحدة Collinear (متوازيين)، فإن الزاوية بينهما θ = 0، وبالتالي سيكون sin θ = 0، وحجم ناتج الجداء الشعاعي يساوي الصفر، ولكن النتيجة هي شعاع؛ وبالتالي تكون نتيجة الجداء الشعاعي هي الشعاع الصفري 0. u × u = 0 بما أن k u‎ على استقامة واحدة مع الشعاع u (حيث k عدد حقيقي)، فسيكون: ‎(k u) × u = 0 نتيجة الجداء الشعاعي u × u = 0 عمودية على كلا المعامَلين، لأن ‎0‎ · u = 0، مما يعني أنهما متعامدان؛ فالشعاع الصفري 0 عمودي على جميع الأشعة كما مر معنا سابقًا. حجم الجداء الشعاعي ‎u × 0‎ وهو ‎| u | | 0 | sin θ يساوي الصفر، والنتيجة هي شعاع، وبالتالي: u × 0 = 0 × u = 0‎ لنوجد الآن الجداء الشعاعي ‎(k u) × v‎ من خلال الاطلاع على الرسم البياني السابق، أو من خلال العمل بالصيغة التالية: ‎(k u) × v = k( u × v‎ ) وسينتج ما يلي: | (k u) × v | = | k u | | v | sin θ = | k | | u | | v | sin θ إذًا يكون حجم ‎(k u) × v أكبر بمقدار | k | عن حجم u × v مع إبقاء الاتجاه نفسه. ليس الجداء الشعاعي للأشعة عملية تجميعية لا يُعَد الجداء الشعاعي للأشعة عمليةً تجميعية، ويمكن رؤية ذلك من خلال النظر إلى الرسم البياني السابق، ثم تشكيل الجداء الشعاعي (‎u × v‎)، ثم إجراء الجداء الشعاعي للناتج مع الشعاع w. نشكّل بعد ذلك حاصل الجداء الشعاعي (‎v × w‎)، ثم نجري الجداء الشعاعي للناتج مع الشعاع u؛ وينتج لدينا ما يلي: ‎(u × v) × w ≠ u × (v × w) لاحظ أن ناتج الجداء الشعاعي للشعاعين u × v من الرسم البياني السابق يساوي الشعاع الصفري 0، لأنهما على استقامة واحدة. إمكانية توزيع الجداء الشعاعي على جمع الأشعة توجد خاصية أخرى للجداء الشعاعي، وهي إمكانية توزيع الجداء الشعاعي على عملية جمع الأشعة كما يلي: u × (v + w) = u × v + u × w‎ ويمكن الوصول إلى هذه الخاصية من خلال التعويض في الصيغة التي تعرّف الجداء الشعاعي. الجداء الشعاعي بين محاور الإحداثيات تسمَّى أشعة الوحدة Unit Vectors التي تكون باتجاه المحاور x و y و z (في أيّ إطار إحداثي مُستخدَم)، i و j و k في أغلب الأحيان، وتوضَع أحيانًا قبعات صغيرة على كل حرف كما في الشكل التالي: بما أن طول كل شعاع من هذه الأشعة يساوي 1، وهي أشعة متعامدة فيما بينها، فإن جيب Sine الزاوية بين أيّ شعاعين منها يساوي 1.0، وستكون الصيغ التالية صحيحة: i × j = k‎ j × k = i‎ k × i = j‎ i × k = - ‎j k × j = - ‎i j × i = - k‎‎ i × i = 0‎ j × j = 0‎ k × k = 0‎ ولكن يُفضَّل رسم الرسم البياني ثم استخدام قاعدة اليد اليمنى بدلًا من حفظها. الجداء الشعاعي للمصفوفات العمودية نعرّف الجداء الشعاعي لمصفوفتين عموديتين ‎u × v‎ كما يلي: يجب أن تمثّل المصفوفتان العموديتان u و v أشعة ثلاثية الأبعاد. تمثّل النتيجة شعاعًا ثلاثي الأبعاد. إذا مثّلنا الشعاع u بالمصفوفة العمودية: ‎u = (ui, uj, uk)T‎ ومثّلنا الشعاع v بالمصفوفة العمودية: ‎v = (vi, vj, vk)T‎ فإن: ‎‎u × v = ( uj vk - uk vj, uk vi - ui vk , ui vj - uj vi )T‎‎ ملاحظة: لا تفكر في حفظ النتيجة السابقة، بل حاول إيجاد نمط محدّد لتشكيلها. لنوجد الآن الجداء الشعاعي ‎(1, 2, 3)T × (0, 0, 0)T‎: لاحظ أن: ‎(1, 2, 3)‎T × 0 = 0 أو يمكنك التعويض في صيغة الجداء الشعاعي للمصفوفات العمودية: ‎(1, 2, 3)T × (0, 0, 0)T = ( 2×0 - 3×0, 3×0 - 1×0, 1×0 - 2×0)T = 0 طريقة مساعدة لتذكر صيغة الجداء الشعاعي للمصفوفات العمودية سنوضّح الآن طريقة لحساب الجداء الشعاعي من خلال ترتيب عناصر كل شعاع في محدّد مصفوفة Determinant، والذي يساعدك لتذكر صيغة الجداء الشعاعي السابقة الصعبة جدًا، ولكن إذا كنت تعرف مسبقًا كيفية تقييم المحدّدات ذات الحجم 3x3، فيمكنك تخطي هذه الطريقة، واستخدام الصيغة الموجودة في التعريف. يحتوي الصف العلوي من المحدّد على الرموز التي تمثل كل محور، ولكن تذكّر أن هذه الطريقة لا تمثّل تعريف الجداء الشعاعي، ولا تمثّل محدد مصفوفة، ولكنها مجرد أداة مساعدة للتذكر. تعمل هذه الطريقة فقط مع الأشعة ثلاثية الأبعاد، إذ لم يُعرَّف الجداء الشعاعي إلّا للأشعة في الفضاء ثلاثي الأبعاد، لذا ضع مكونات الشعاع الأول في الصف الثاني من المحدّد، ومكونات الشعاع الثاني في الصف الثالث. تدريب عملي تدريب1: لنوجِد الجداء الشعاعي للمصفوفتين العموديتين ‎(1, 2, 1)T‎ و ‎(0, -1, 2)T‎ كما يلي: ولنقيّم الآن النتيجة من خلال البحث عن العوامل المساعدة للمكوّن i و j و k التي أحطناها بدائرة حمراء في الصف العلوي؛ فالعوامل المساعدة للمكوّن i مثلًا هي عناصر الصفين 2 و 3 التي ليست موجودة في العمود i. يمكن إيجاد العامل المساعد من خلال ضرب العنصرين الموجودين على القطر الرئيسي، ثم طرح حاصل ضرب العنصرين الموجودين على القطر الآخر من نتيجة ضرب العنصرين الموجودين على القطر الرئيسي، فعلى سبيل المثال، يكون العامل المساعد للمكوّن i هو 2×2 - 1×(-1) = 2 + 1 = 5. تدريب2: يكون حاصل الجداء الشعاعي للمصفوفتين العموديتين ‎(0, -1, 2)T‎ و ‎(1, 2, 1)T‎ كما يلي (لاحظ أننا عكسنا ترتيب الشعاعين السابقين): يتبع تقييم هذا المثال نمط المثال السابق نفسه، ولكن مع تبديل الصفين الثاني والثالث كما يلي: لاحظ أن النتيجة تمثّل معاكس النتيجة السابقة، مما يدل على أن: u × v = -(v × u)‎ ولقد رأينا ذلك سابقًا عندما استخدمنا الأشعة الهندسية. الجداء الشعاعي لمصفوفة عمودية مع نفسها يبقى الصفان الثاني والثالث كما هما عندما نأخذ الجداء الشعاع لشعاعٍ مع نفسه، وتذكّر من دروس الرياضيات في المرحلة الثانوية أنه إذا كان صفان من المحدّد متماثلين، فستكون قيمته صفرًا؛ حيث يكون كل عامل مساعد صفرًا، مما يؤدي إلى أن: 0‎ i + 0 j + 0 k = 0‎‎ يحدث الشيء نفسه إذا كان أحد الصفوف مضاعفًا للآخر؛ إذ تعكس هذه النتائج ما رأيناه مسبقًا مع الأشعة الهندسية: k u × u = 0‎ تنطبق أيضًا الخاصيات الأخرى لحاصل الجداء الشعاعي للأشعة الهندسية على حاصل الجداء الشعاعي لتمثيلاتها باستخدام المصفوفات العمودية. وصلنا إلى نهاية هذا المقال الذي تعرّفنا فيه على كيفية حساب الجداء الشعاعي للأشعة، وسنتعرّف في المقال التالي على المصفوفات المستطيلة وعملياتها. ترجمة -وبتصرُّف- للفصل Vector Cross Product من كتاب Vector Math for 3D Computer Graphics لصاحبه Bradley Kjell. اقرأ أيضًا المقال السابق: إسقاط شعاع على شعاع آخر واستخداماته في الرسوميات الحاسوبية 3D تغيير حجم شعاع Scaling في التصاميم ثلاثية الأبعاد 3D الجداء النقطي وارتباطه بطول الأشعة وتعامدها في التصاميم 3D
  20. يمكن أن تخدع صيغة بايثون البسيطة وسهلة التعلم مطوري لغة بايثون Python وخاصة الجدد منهم، مما يؤدي إلى تفويت بعض التفاصيل الدقيقة والتقليل من قوة اللغة، لذا سنقدّم في هذا المقال قائمة بأكثر 10 أخطاء شيوعًا، والتي تكون دقيقة ويصعب اكتشافها ويمكن أن تخدع حتى مطور بايثون الأكثر تقدمًا. مقدمة إلى بايثون تُعَد بايثون لغة برمجة مُفسَّرة Interpreted وكائنية التوجّه Object-oriented وعالية المستوى ولها دلالات Semantics ديناميكية، وتجعل هياكلُ البيانات المُضمَّنة عالية المستوى، والتحقق الديناميكي من الأنواع، والربط الديناميكي من لغة بايثون جذابة للغاية لتطوير التطبيقات بسرعة، بالإضافة إلى استخدامها بوصفها لغة برمجة لكتابة السكربتات أو لغة لاصقة Glue Language لوصل المكونات أو الخدمات الموجودة مسبقًا مع بعضها البعض. كما تدعم لغة بايثون الوحدات والحزم، وبالتالي تشجع التقسيم إلى وحدات Modularity وإعادة استخدام الشيفرة البرمجية. ملاحظة: هذا المقال مخصَّص للمبرمجين المحترفين في بايثون، وليس موجَّهًا للمطورين الجدد الذين قد يكونون أقل دراية بأخطاء بايثون الشائعة. الخطأ 1: استخدام التعابير بوصفها قيمًا افتراضية لوسطاء الدوال بطريقة خاطئة تسمح لغة بايثون بتحديد وسيط الدالة بأنه اختياري من خلال توفير قيمة افتراضية له، ولكن قد تؤدي هذه الميزة إلى بعض الارتباك عندما تكون القيمة الافتراضية متغيرة بالرغم من أن هذه ميزة رائعة لهذه اللغة. إليك تعريف دالة بايثون التالي مثلًا: >>> def foo(bar=[]): # يُعد الوسيط‫ bar اختياريًا وقيمته الافتراضية هي [] عند عدم تحديدها ... bar.append("baz") # ولكن يمكن أن يسبّب هذا السطر مشكلة كما سنرى لاحقًا‫... ... return bar من الأخطاء الشائعة أن نعتقد أن الوسيط الاختياري مضبوط على التعبير الافتراضي المحدَّد في كل مرة تُستدعَى فيها الدالة دون توفير قيمة لهذا الوسيط الاختياري، فمثلًا قد نتوقع في الشيفرة البرمجية السابقة أن استدعاء الدالة foo()‎ بصورة متكررة (أي بدون تحديد الوسيط bar) سيؤدي دائمًا إلى إعادة القيمة 'baz'، بما أننا اعتقدنا أن الوسيط bar مضبوط على القيمة [] (أي قائمة فارغة جديدة) في كل مرة نستدعي فيها الدالة foo()‎ (بدون تحديد الوسيط bar)، ولكن لنلقِ نظرة على ما يحدث فعليًا: >>> foo() ["baz"] >>> foo() ["baz", "baz"] >>> foo() ["baz", "baz", "baz"] لاحظ استمرار إلحاق القيمة الافتراضية "baz" إلى القائمة الموجودة مسبقًا في كل مرة نستدعي فيها الدالة foo()‎ بدلًا من إنشاء قائمة جديدة في كل مرة، إذ تُقيَّم القيمة الافتراضية لوسيط الدالة مرة واحدة فقط في وقت تعريف الدالة، وبالتالي يُهيَّأ الوسيط bar على قيمته الافتراضية (أي قائمة فارغة) عند تعريف الدالة foo()‎ لأول مرة فقط، ولكن ستستمر بعد ذلك استدعاءات الدالة foo()‎ (بدون تحديد الوسيط bar) في استخدام القائمة نفسها التي هيّأنا بها الوسيط bar في الأصل. الحل الشائع لهذه المشكلة هو ما يلي: >>> def foo(bar=None): ... if bar is None: # ‫أو if not bar:‎‫ ... bar = [] ... bar.append("baz") ... return bar ... >>> foo() ["baz"] >>> foo() ["baz"] >>> foo() ["baz"] الخطأ 2: استخدام متغيرات الصنف Class استخدامًا خاطئًا ليكن لدينا المثال التالي: >>> class A(object): ... x = 1 ... >>> class B(A): ... pass ... >>> class C(A): ... pass ... >>> print A.x, B.x, C.x 1 1 1 وبالتالي سيكون لدينا أيضًا ما يلي كما هو متوقع: >>> B.x = 2 >>> print A.x, B.x, C.x 1 2 1 ولكن سيكون لدينا ما يلي: >>> A.x = 3 >>> print A.x, B.x, C.x 3 2 3 لاحظ تغيير قيمة C.x بالرغم من أننا غيرنا قيمة A.x فقط، حيث تُعامَل متغيرات الصنف داخليًا على أنها قواميس في لغة بايثون وتتبع ما يشار إليه غالبًا باسم ترتيب تحليل التوابع أو ترتيب استبيان التوابع Method Resolution Order -أو MRO اختصارًا وهو الآلية التي تستخدمها لغات البرمجة ومن ضمنها بايثون لتحديد ترتيب البحث عن التوابع في التسلسل الهرمي hierarchy الخاص بالكائنات في حالة استخدام الوراثة المتعددة، أي أنه يحدد المسار الذي سيتبعه البرنامج عند محاولة استدعاء دالة معينة موجودة في أكثر من صنف أو الوراثة من عدة أصناف. لذلك سنبحث عن السمة Attribute التي هي x في أصنافها الأساسية (أي الصنف A فقط في المثال السابق بالرغم من أن لغة بايثون تدعم الوراثة المتعددة) بما أننا لم نعثر على هذه السمة في الصنف C. يمكن القول أيضًا أن الصنف C ليس لديه الخاصية x الخاصة به والمستقلة عن الصنف A، وبالتالي لا يُعَد المرجع إلى C.x هو المرجع نفسه إلى A.x، ويؤدي ذلك إلى حدوث مشكلة في بايثون إن لم نتعامل معها بطريقة صحيحة. الخطأ 3: تحديد المعاملات لكتلة الاستثناء Exception بطريقة خاطئة لنفترض أن لدينا الشيفرة البرمجية التالية: >>> try: ... l = ["a", "b"] ... int(l[2]) ... except ValueError, IndexError: # لالتقاط الاستثناءَين ... pass ... Traceback (most recent call last): File "<stdin>", line 3, in <module> IndexError: list index out of range المشكلة في المثال السابق هي أن تعليمة except لا تأخذ قائمة الاستثناءات المُحدَّدة بهذه الطريقة، حيث تستخدم بايثون الصيغة except Exception, e لربط الاستثناء بالمعامل الثاني الاختياري المُحدَّد (هو e في هذه الحالة)، وبالتالي يمكن إتاحته لمزيد من الفحص. لم تلتقط التعليمة except الاستثناء IndexError، بل يُربَط الاستثناء بمعاملٍ اسمه IndexError، وتُعَد مثل هذه الأخطاء شائعة في شيفرة بايثون البرمجية. الطريقة الصحيحة لالتقاط الاستثناءات المتعددة في التعليمة except هي تحديد المعامل الأول بوصفه مجموعة Tuple تحتوي على جميع الاستثناءات المُلتقَطة. يمكن تحقيق أقصى قدر من قابلية النقل من خلال استخدام الكلمة المفتاحية as لأن هذه الصيغة تدعمها Python 2 و Python 3: >>> try: ... l = ["a", "b"] ... int(l[2]) ... except (ValueError, IndexError) as e: ... pass ... >>> الخطأ 4: سوء فهم قواعد نطاق Scope بايثون يعتمد تحليل Resolution نطاق بايثون على قاعدة LEGB، وهي اختصار للكلمات محلي ‎Local وشامل ‎Enclosing وعام ‎Global ومُضمَّن ‎Built-in. توجد بعض التفاصيل الدقيقة للطريقة التي تعمل بها هذه القاعدة في بايثون، مما يقودنا إلى مشكلة برمجة بايثون الشائعة الأكثر تقدمًا التالية، فليكن لدينا ما يلي مثلًا: >>> x = 10 >>> def foo(): ... x += 1 ... print x ... >>> foo() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in foo UnboundLocalError: local variable 'x' referenced before assignment يحدث الخطأ السابق لأنه إذا أسندتَ قيمة إلى متغير في نطاقٍ ما، فستَعُد لغة بايثون هذا المتغير متغيرًا محليًا لذلك النطاق تلقائيًا وتظلّل أيّ متغير يحمل الاسم نفسه في أيّ نطاق خارجي. يتفاجأ الكثير من المبرمجين بالحصول على الخطأ UnboundLocalError في الشيفرة البرمجية التي عملت بنجاح سابقًا عند تعديلها من خلال إضافة تعليمة إسناد في مكانٍ ما من جسم الدالة، فمن الشائع أن يؤدي ذلك إلى أن يخطئ المطورون عند استخدام القوائم خاصةً كما في المثال التالي: >>> lst = [1, 2, 3] >>> def foo1(): ... lst.append(5) # تعمل هذه التعليمة بنجاح ... >>> foo1() >>> lst [1, 2, 3, 5] >>> lst = [1, 2, 3] >>> def foo2(): ... lst += [5] # ولكن تعطي هذه التعليمة خطأً ... >>> foo2() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in foo UnboundLocalError: local variable 'lst' referenced before assignment لاحظ أن الدالة foo1 تعمل بنجاح بينما تعطي الدالة foo2 خطأ، والسبب في ذلك هو مماثل لمشكلة المثال السابق ولكنه أكثر دقة، حيث لا تسند الدالة foo1 قيمة إلى المتغير lst على عكس الدالة foo2، فالتعليمة lst += [5]‎ هي مجرد اختصار للتعليمة lst = lst + [5]‎ التي تمثّل محاولة إسناد قيمة إلى المتغير lst، وبالتالي تفترض لغة بايثون أن هذا المتغير موجود في النطاق المحلي، ولكن تعتمد القيمة التي نريد إسنادها إلى المتغير lst على المتغير lst نفسه الذي يُفترَض وجوده في النطاق المحلي ولم نعرّفه بعد. الخطأ 5: تعديل القائمة أثناء المرور عليها يجب أن تكون مشكلة الشيفرة البرمجية التالية واضحة إلى حدٍ ما: >>> odd = lambda x : bool(x % 2) >>> numbers = [n for n in range(10)] >>> for i in range(len(numbers)): ... if odd(numbers[i]): ... del numbers[i] # تصرف سيء: حذف عنصر من القائمة أثناء المرور عليها ... Traceback (most recent call last): File "<stdin>", line 2, in <module> IndexError: list index out of range يُعَد حذف عنصر من قائمة أو مصفوفة أثناء المرور عليها مشكلةً معروفةً جيدًا في بايثون لمطوري البرمجيات أصحاب الخبرة، ولكن بالرغم من أن المثال السابق قد يكون واضحًا إلى حدٍ ما، إلّا أنه يمكن أن يرتكب حتى المطورون المتقدمون عن غير قصد هذا الخطأ في الشيفرة البرمجية الأكثر تعقيدًا. لحسن الحظ، تتضمن لغة بايثون عددًا من نماذج البرمجة الأنيقة التي يمكن أن تؤدي إلى شيفرة برمجية مبسطة ومنظَّمة بصورة كبيرة عند استخدامها استخدامًا صحيحًا، ممّا يقلل من احتمالية وجود خطأ الحذف غير المقصود لعنصر القائمة أثناء المرور عليها في هذه الشيفرة البرمجية الأبسط. أحد هذه النماذج هو نموذج استيعاب القوائم List Comprehensions الذي يُعَد مفيدًا خاصةً لتجنب هذه المشكلة كما هو موضّح في التطبيق البديل التالي للشيفرة البرمجية السابقة والذي يعمل بطريقة مثالية: >>> odd = lambda x : bool(x % 2) >>> numbers = [n for n in range(10)] >>> numbers[:] = [n for n in numbers if not odd(n)] # الحل هنا >>> numbers [0, 2, 4, 6, 8] الخطأ 6: عدم وضوح كيفية ربط Bind بايثون للمتغيرات في المنغلقات Closures ليكن لدينا المثال التالي: >>> def create_multipliers(): ... return [lambda x : i * x for i in range(5)] >>> for multiplier in create_multipliers(): ... print multiplier(2) ... قد نتوقع الخرج التالي للشيفرة البرمجية السابقة: 0 2 4 6 8 لكننا سنحصل على ما يلي: 8 8 8 8 8 يحدث ذلك بسبب سلوك الربط المتأخر Late Binding لبايثون الذي يبحث عن قيم المتغيرات المُستخدَمة في المنغلقات Closures في وقت استدعاء الدالة الداخلية، لذلك إذا استدعينا أيًا من الدوال المُعادة في المثال السابق، فسيُجرَى البحث عن قيمة المتغير i في النطاق المحيط في وقت استدعائها، حيث ستكون الحلقة قد اكتملت عندها، لذلك أُسنِدت القيمة 4 إلى المتغير i فعليًا. ويكون حل هذه المشكلة الشائعة في بايثون كما يلي: >>> def create_multipliers(): ... return [lambda x, i=i : i * x for i in range(5)] ... >>> for multiplier in create_multipliers(): ... print multiplier(2) ... 0 2 4 6 8 استفدنا من الوسطاء الافتراضية لإنشاء دوال مجهولة لتحقيق السلوك المطلوب. قد يَعُد البعض هذه الطريقة مناسبة، وقد يَعدها البعض رائعة، وقد يكرهها البعض الآخر، ولكن من المهم أن تفهمها إذا كنت مطور بايثون. الخطأ 7: إنشاء اعتماديات Dependencies الوحدات الدائرية لنفترض أن لدينا الملفان a.py و b.py، حيث يستورد كلّ منهما الآخر كما يلي: في الملف a.py: import b def f(): return b.x print f() وفي الملف b.py: import a x = 1 def g(): print a.f() أولًا، لنحاول استيراد الوحدة a.py كما يلي: >>> import a 1 لاحظ أن عملية الاستيراد نجحت، وقد يكون ذلك مفاجأة لك، فلدينا استيراد دائري هنا والذي يُفترَض أن يمثل مشكلة، أليس كذلك؟ ولكن لا يمثّل مجرد وجود استيراد دائري في حد ذاته مشكلة في بايثون، فلغة بايثون ذكية بما يكفي لعدم محاولة إعادة استيراد وحدة إذا كانت مستوردةً فعليًا، ولكنك قد تواجه مشكلات اعتمادًا على النقطة التي تحاول فيها كل وحدة الوصول إلى الدوال أو المتغيرات المُعرَّفة في الوحدة الأخرى. لم يكن هناك مشكلة في استيراد الوحدة b.py لأنها لا تتطلب تعريف أيّ شيء من الوحدة a.py في وقت استيرادها عندما استوردنا الوحدة a.py في المثال السابق، فالإشارة الوحيدة إلى الوحدة a في الملف b.py هو استدعاء الدالة a.f()‎، ولكن هذا الاستدعاء موجود في الدالة g()‎ ولا يوجد شيء في الملفين a.py أو b.py يستدعي الدالة g()‎، لذا لا يوجد شيء يدعو للقلق. ولكن إذا حاولنا استيراد الوحدة b.py دون استيراد الوحدة a.py مسبقًا كما يلي: >>> import b Traceback (most recent call last): File "<stdin>", line 1, in <module> File "b.py", line 1, in <module> import a File "a.py", line 6, in <module> print f() File "a.py", line 4, in f return b.x AttributeError: 'module' object has no attribute 'x' فستظهر مشكلة تتمثّل في أن الوحدة b.py تحاول استيراد الوحدة a.py عند عملية استيراد الوحدة b.py، وتستدعي الوحدة a.py بدورها الدالة f()‎ التي تحاول الوصول إلى المتغير b.x الذي لم نعرّفه بعد، وبالتالي سيظهر الاستثناء AttributeError. توجد حلول مختلفة لهذا الخطأ، وسيكون أحد هذه الحلول على الأقل بسيطًا، فمثلًا عدّل الوحدة b.py لتستورد الوحدة a.py ضمن الدالة g()‎: x = 1 def g(): import a # ستُقيَّم هذه التعليمة عند استدعاء الدالة‫ g()‎ فقط print a.f() وإذا استوردناه هذه الوحدة، فسيكون كل شيء على ما يرام كما يلي: >>> import b >>> b.g() 1 # يُطبَع لأول مرة بسبب استدعاء الوحدة‫ 'a' للتعليمة 'print f()‎' في النهاية 1 # يُطبَع مرة ثانية، حيث يمثّل استدعاء الدالة‫ 'g' الخطأ 8: تعارض الأسماء مع وحدات مكتبة بايثون المعيارية تتميز لغة بايثون بوفرة وحدات المكتبات التي تأتي معها، ولكن قد يؤدي ذلك إلى الوقوع في تعارض في الأسماء بين اسم إحدى الوحدات الخاصة بك ووحدة أخرى تحمل الاسم نفسه في المكتبة المعيارية التي تأتي مع لغة بايثون إن لم تكن حذرًا، فمثلًا قد يكون لديك وحدة بالاسم email.py في شيفرتك البرمجية، والتي قد تتعارض مع وحدة المكتبة المعيارية التي تحمل الاسم نفسه. يمكن أن يؤدي ذلك إلى مشكلات خطيرة مثل استيراد مكتبة أخرى، والتي تحاول بدورها استيراد إصدارٍ من وحدة خاصة بمكتبة بايثون المعيارية، ولكن إذا كان لديك وحدة تحمل الاسم نفسه، فستستورد الحزمة الأخرى الإصدار الخاص بك عن طريق الخطأ بدلًا من الإصدار الموجود في مكتبة بايثون المعيارية، مما يؤدي إلى حدوث أخطاء، لذا يجب توخي الحذر لتجنب استخدام الأسماء نفسها الخاصة بوحدات مكتبة بايثون المعيارية. من الأسهل بالنسبة لك تغيير اسم الوحدة ضمن الحزمة الخاصة بك بدلًا من تقديم اقتراح تحسين بايثون Python Enhancement Proposal -أو PEP اختصارًا- لطلب تغيير الاسم ومحاولة الحصول على الموافقة على ذلك. الخطأ 9: الفشل في معالجة الاختلافات بين الإصدارين Python 2 و Python 3 ليكن لدينا الملف foo.py التالي مثلًا: import sys def bar(i): if i == 1: raise KeyError(1) if i == 2: raise ValueError(2) def bad(): e = None try: bar(int(sys.argv[1])) except KeyError as e: print('key error') except ValueError as e: print('value error') print(e) bad() يعمل ما يلي بنجاح في Python 2: $ python foo.py 1 key error 1 $ python foo.py 2 value error 2 ولكنه يعطي خطأً في Python 3 كما يلي: $ python3 foo.py 1 key error Traceback (most recent call last): File "foo.py", line 19, in <module> bad() File "foo.py", line 17, in bad print(e) UnboundLocalError: local variable 'e' referenced before assignment المشكلة هي أنه لا يمكن الوصول إلى كائن الاستثناء خارج نطاق كتلة التعليمة except في Python 3، وإلّا فيجب الاحتفاظ بدورة مرجعية مع إطار المكدس في الذاكرة حتى تشغيل كانس المهملات Garbage Collector وإزالة المراجع من الذاكرة. إحدى الطرق لتجنب هذه المشكلة هي الاحتفاظ بمرجع إلى كائن الاستثناء خارج نطاق كتلة التعليمة except بحيث يبقى قابلًا للوصول. إليك فيما يلي نسخة من المثال السابق الذي يستخدم هذه التقنية، وبالتالي ستنتج شيفرة برمجية متوافقة مع Python 2 و Python 3: import sys def bar(i): if i == 1: raise KeyError(1) if i == 2: raise ValueError(2) def good(): exception = None try: bar(int(sys.argv[1])) except KeyError as e: exception = e print('key error') except ValueError as e: exception = e print('value error') print(exception) good() لنشغّل هذه الشيفرة البرمجية على الإصدار Py3k: $ python3 foo.py 1 key error 1 $ python3 foo.py 2 value error 2 اطّلع على مقال كيفية ترحيل شيفرة بايثون 2 إلى بايثون 3 لمزيد من المعلومات. الخطأ 10: استخدام التابع del بطريقة خاطئة لنفترض أن لدينا ما يلي في ملفٍ اسمه mod.py: import foo class Bar(object): ... def __del__(self): foo.cleanup(self.myhandle) ثم حاولنا استيراده من الملف another_mod.py كما يلي: import mod mybar = mod.Bar() فسنحصل على الاستثناء AttributeError، والسبب هو ضبط جميع متغيرات الوحدة العامة على القيمة None عند إيقاف تشغيل المفسّر Interpreter، لذلك ضُبِط الاسم foo على القيمة None عند استدعاء التابع __del__ في المثال السابق. الحل لهذه المشكلة هو استخدام الدالة atexit.register()‎ بدلًا من ذلك، وبالتالي ستُشغَّل معالجاتك المسجَّلة قبل إيقاف تشغيل المفسِّر عندما ينتهي برنامجك من التنفيذ (أي عند الخروج منه بطريقة طبيعية). إذًا لنصلِح شيفرة mod.py البرمجية السابقة كما يلي: import foo import atexit def cleanup(handle): foo.cleanup(handle) class Bar(object): def __init__(self): ... atexit.register(cleanup, self.myhandle) يوفّر هذا المثال طريقة نظيفة وموثوقة لاستدعاء أيّ دالة تنظيف مطلوبة عند إنهاء البرنامج العادي، ومن الواضح أن الأمر متروك للدالة foo.cleanup لتحديد ما يجب فعله بالكائن المرتبط بالاسم self.myhandle. مخاطر بايثون يمكن تجنبها من خلال معرفة الفروق الأساسية تُعَد بايثون لغة قوية ومرنة وتحتوي على العديد من الآليات والنماذج التي يمكن أن تحسّن الإنتاجية بصورة كبيرة، ولكن يمكن أن يكون الفهم أو التقدير المحدود لقدراتها في بعض الأحيان عائقًا أكثر من كونه فائدة كما هو الحال مع أيّ أداة أو لغة برمجية، حيث يعتقد الشخص في أن يعلم ما يكفي، ولكنه يشكّل خطرًا. سيساعد التعرف على الفروق الأساسية في لغة بايثون -مثل مشاكل البرمجة المتقدمة التي ذكرناها في هذا المقال- على تحسين استخدام اللغة مع تجنب بعض الأخطاء في بايثون. ترجمة -وبتصرُّف- للمقال The 10 Most Common Python Code Mistakes لصاحبه Martin Chikilian. اقرأ أيضًا مصطلحات شائعة مثيرة للالتباس في بايثون تنقيح أخطاء Debugging شيفرتك البرمجية باستخدام لغة بايثون كيف تستخدم منقح بايثون فهم العمليات المنطقية في بايثون 3 أشهر 10 مشكلات تواجه مبرمجي لغة جافا سكريبت JavaScript
  21. يُعَد لارافيل Laravel إطار ويب يستخدم لغة PHP، وهو مجاني ومفتوح المصدر ويُستخدَم على نطاق واسع لتطوير تطبيقات الويب، كما أنه معروف بصيغته الأنيقة وأدواته المُستخدَمة للمهام الشائعة مثل التوجيه Routing والاستيثاق Authentication والتخزين المؤقت أو التخبئة Caching وقدرته على التعامل مع حركة الزوار العالية. يتبع لارافيل معمارية نموذج-عرض-متحكم Model-View-Controller (أو MVC اختصارًا)، ويتضمن دعمًا مُدمجًا لربط الكائنات بالعلاقات Object-Relational Mapping (أو ORM اختصارًا) وباني الاستعلامات، مما يسهّل التفاعل مع قواعد البيانات. يضم لارافيل مجتمعًا كبيرًا ونشطًا يوفّر مجموعةً كبيرة من البرامج التعليمية والحزم والموارد الأخرى التي يمكن للمطورين استخدامها، إذ يُعَد لارافيل أحد أطر عمل الويب الأكثر شعبية وقوة للغة PHP، وتثق به العديد من الشركات والمؤسسات لبناء تطبيقات ويب قوية وقابلة للتوسّع. سنشرح في هذا المقال الاستخدام الأساسي لإطار عمل لارافيل من خلال بناء نظام تدوين بسيط. ملاحظة: تعتمد هذه السلسلة من المقالات على الإصدار 11 من لارافيل. إنشاء مشروع لارافيل جديد لا يُعَد إعداد بيئة تطوير PHP مهمة سهلة، وخاصة إذا استخدمت أنظمة لينكس Linux، ولكن قدّم لارافيل منذ الإصدار 8.0 أداة جديدة اسمها Sail، والتي توفر حلًا سهلًا لتشغيل تطبيقات لارافيل من خلال استخدام أداة دوكر Docker بغض النظر عن نظام التشغيل الذي تستخدمه، طالما أن هذه الأداة مُثبَّتة لديك. أنشئ أولًا مجلد عمل جديد بالاسم laravel-tutorial مثلًا، ثم افتحه في محرّر الأكواد الذي تفضله وليكن VS Code، ونفّذ الأمر الآتي لإنشاء مشروع لارافيل جديد، وتأكّد من تشغيل دوكر قبل تنفيذ هذا الأمر، إذ قد تستغرق هذه العملية من 5 إلى 10 دقائق حتى الاكتمال: curl -s https://laravel.build/<app_name> | bash ملاحظة: إذا كنت تعمل على نظام ويندوز Windows، فتأكّد من تشغيل هذا الأمر ضمن نظام ويندوز الفرعي لنظام التشغيل لينكس WSL 2 الذي يسمح لك بتشغيل نواة Linux كاملة داخل ويندوز دون الحاجة إلى تثبيت نظام لينكس. ثانيًا، استخدم الأمر التالي للانتقال إلى مجلد التطبيق وشغّل الخادم: cd <app_name> سيؤدي الأمر التالي إلى بدء حاوية دوكر بالإضافة إلى خادم تطوير لارافيل: ./vendor/bin/sail up يمكنك الوصول إلى تطبيق لارافيل على العنوان http://localhost/‎، وإن لم يعمل هذا العنوان، فحاول الانتقال إلى العنوان http://127.0.0.1/‎ بدلًا من ذلك، ثم يجب أن تشاهد صفحة ترحيب لارافيل التالية: استكشاف بنية تطبيق لارافيل لنلقِ نظرة على المجلدات والملفات التي تنشأ ضمن مشروعنا قبل أن نبدأ ببرمجته، لذا إليك نظرة عامة على المجلد الجذر للمشروع: . ├── app │ ├── Console │ ├── Exceptions │ ├── Http │ │ ├── Controllers │ │ └── Middleware │ ├── Models │ └── Providers ├── bootstrap ├── config ├── database │ ├── factories │ ├── migrations │ └── seeders ├── public ├── resources │ ├── css │ ├── js │ └── views ├── routes ├── storage │ ├── app │ ├── framework │ └── logs ├── tests │ ├── Feature │ └── Unit └── vendor المجلد app: يُعَد هذا المجلد المكون الأساسي لمشروعنا، ويحتوي على مجلدات متعددة من أهمها مجلد المتحكمات controllers والبرمجيات الوسيطة middleware والنماذج models، حيث يعرّف المتحكم الشيفرة البرمجية الأساسية للتطبيق، وتعرّف البرمجيات الوسيطة الإجراءات التي يجب اتخاذها قبل استدعاء المتحكم، ويوفّر النموذج واجهة تسمح لنا بالتعامل مع قواعد البيانات، إذ سنتحدث عن كل منها بالتفصيل لاحقًا. المجلد bootstrap: يحتوي هذا المجلد على الملف app.php الذي يشغّل المشروع بأكمله، ولا حاجة لتعديل أيّ شيء في هذا المجلد. المجلد config: يحتوي على ملفات الضبط Configuration كما يدل اسمه، ولا حاجة للاهتمام بهذا الضبط في هذه السلسلة من المقالات. المجلد database: يحتوي على ملفات التهجير migrations والمصانع factories والبذور seeds، حيث تصف ملفات التهجير بنية قاعدة البيانات، وتُعَد ملفات المصانع والبذور طريقتين مختلفتين يمكننا من خلالهما ملء قاعدة البيانات ببيانات وهمية في لارافيل. المجلد public: يحتوي على الملف index.php الذي يُعَد نقطة الدخول إلى لتطبيق. المجلد resources: يحتوي على ملفات العرض views التي تمثّل جزء الواجهة الأمامية من تطبيق لارافيل. المجلد routes: يحتوي على جميع الموجّهات Routers لعناوين URL للمشروع فعندما ترسل طلب إلى عنوان URL محدد، سيتوجه الطلب إلى المُوجِّه المناسب بناءً على الوجهات التي تحددها الملفات ضمن هذا المجلد. المجلد storage: هو مساحة تخزين المشروع بأكمله، ويحتوي على السجلات logs والعروض المُصرَّفة Compiled Views بالإضافة إلى الملفات التي رفعها المستخدم. المجلد tests: يحتوي على ملفات الاختبار، حيث يُعَد الاختبار مفهومًا أكثر تقدمًا نسبيًا في لارافيل، لذلك لن نوضّحه في هذه السلسلة من المقالات، ولكن يمكنك الاطلاع على مقال كيفية استخدام PHPUnit لاختبار تطبيقات لارافيل لمزيد من التفاصيل حول الاختبارات. المجلد vendor: يتضمن جميع الاعتماديات Dependencies. متغيرات البيئة سنلقي نظرة أيضًا على متغيرات البيئة التي تُخزَّن في ملف ‎.env، حيث أُعِدت الكثير من عمليات الضبط افتراضيًا عندما أنشأنا مشروعنا باستخدام أداة Sail في لارافيل، ولكننا سنتحدث عن عملها فيما يلي. المتغير APP_URL يحدّد المتغير APP_URL عنوان URL للتطبيق، ويكون هذا العنوان هو http://localhost افتراضيًا، ولكن قد تحتاج إلى تغييره إلى العنوان http://127.0.0.1 عندما يعطي العنوانُ http://127.0.0.1 صفحةَ خادم Apache2 الافتراضية. APP_URL=http://127.0.0.1 أو: APP_URL=http://localhost قاعدة البيانات ستثبّت أداة Sail من لارافيل نظامَ إدارة قواعد البيانات MySQL بوصفه التطبيق الخاص بقاعدة البيانات، وتُعرَّف اتصالات قاعدة البيانات كما يلي: DB_CONNECTION=mysql DB_HOST=mysql DB_PORT=3306 DB_DATABASE=curl_demo DB_USERNAME=sail DB_PASSWORD=password يمكنك أيضًا تعريف متغيرات بيئة خاصة بك إن احتجت إليها كما يلي، ثم يمكنك الوصول إليها في أي مكان من المشروع: CUSTOM_VARIABLE=true ويمكن الوصول إلى هذا المتغير كما يلي: env('CUSTOM_VARIABLE', true) يمثّل المعامل الثاني القيمة الافتراضية لهذا المتغير، حيث إن لم يكن المتغير CUSTOM_VARIABLE موجودًا، فسيُعاد المعامل الثاني. أساسيات التوجيه Routing في لارافيل سنلقي نظرة في هذا القسم على وِجهات Route لارافيل وبرمجياتها الوسيطة، حيث تُعرَّف الوِجهات في المجلد routes. لاحظ وجود أربعة ملفات مختلفة في المجلد routes افتراضيًا، ولكن ما يهمنا الملفان api.php و web.php بالنسبة لمعظم المشاريع. إذا أردت استخدام لارافيل بطريقة صارمة مع الواجهة الخلفية (بدون العرض)، فيجب أن تحدّد الوِجهات في الملف api.php، ولكننا سنستخدم في هذا المقال لارافيل بوصفه إطار عمل متكامل full-stack، لذلك سنستخدم الملف web.php بدلًا من ذلك. الفرق بين هذين الملفين هو أن الملف api.php تغلِّفه مجموعة برمجيات api الوسيطة وأن الملف web.php موجود ضمن مجموعة برمجيات web الوسيطة، ويوفر هذان الملفان دوالًا مختلفة، وسيكون للوِجهات المحددة في الملف api.php بادئة عنوان URL التي هي /api/، وبالتالي يجب أن يكون عنوان URL مثل العنوان: http://example.com/api/somthing-else للوصول إلى وِجهة api. تقبل الوِجهة الأساسية في لارافيل عنوان URL ثم تعيد قيمة، حيث يمكن أن تكون هذه القيمة سلسلة نصية أو عرضًا أو متحكمًا. انتقل إلى الملف routes/web.php، وسترى أن هناك وِجهة مُعرَّفة مسبقًا هي: routes/web.php. يخبرنا الجزء التالي من الشيفرة البرمجية أنه إذا استقبل لارافيل الوِجهة "/"، فسيعيد عرضًا بالاسم "welcome"، والذي يقع في resources/views/welcome.blade.php: use Illuminate\Support\Facades\Route; Route::get('/', function () { return view('welcome'); }); افتح المتصفح، وانتقل إلى العنوان http://127.0.0.1:8000، وستحصل على الصفحة التالية: يمكن التحقق من أن welcome.blade.php هو العرض الظاهر من خلال إجراء بعض التغييرات على الملف، ثم تحديث متصفحك للتأكد من تغيير الصفحة. توابع الموجهات لنلقِ الآن نظرة على الموجِّه التالي في routes/web.php ونفهم كيفية عمله: use Illuminate\Support\Facades\Route; Route::get('/', function () { return view('welcome'); }); نستورد أولًا الصنف Route ونستدعي التابع get()‎، حيث يتطابق هذا التابع مع تابع HTTP الذي هو GET، وهناك توابع أخرى مُضمَّنة في الصنف Route، حيث تتطابق هذه التوابع مع توابع طلبات HTTP الأخرى كما يلي: Route::get($uri, $callback); Route::post($uri, $callback); Route::put($uri, $callback); Route::patch($uri, $callback); Route::delete($uri, $callback); Route::options($uri, $callback); إذا أردتَ أن تطابق الوِجهة بين توابع HTTP متعددة، فيمكنك استخدام تابع match أو any بدلًا من ذلك، حيث يتطلب التابع match()‎ تحديد مصفوفة توابع HTTP التي ترغب في مطابقتها، ويطابق التابع any()‎ ببساطة بين جميع طلبات HTTP. Route::match(['get', 'post'], '/', function () { . . . }); Route::any('/', function () { . . . }); تمرير البيانات إلى العرض يحتوي التابع get()‎ على معاملَين هما: المعامل الأول هو عنوان URL الذي يُفترَض أن يطابقه الموجِّه، والمعامل الثاني هو دالة رد النداء Callback التي تُنفَّذ عند نجاح المطابقة. تُعاد الدالة view()‎ المُضمَّنة ضمن دالة رد النداء، حيث ستبحث هذه الدالة عن ملف العرض المقابل بناءً على المعامل المُمرَّر إليها. يقدم لارافيل اختصارًا بسيطًا هو Route::view()‎ إذا أردتَ إعادة عرض فقط، حيث يسمح لك هذا التابع بتجنب كتابة وِجهة كاملة كما يلي: Route::view('/welcome', 'welcome'); الوسيط الأول لهذا التابع هو عنوان URL، والمعامل الثاني هو العرض المقابل، ويوجد أيضًا وسيط ثالث يسمح بتمرير بعض البيانات إلى هذا العرض كما يلي: Route::view('/welcome', 'welcome', ['name' => 'Taylor']); سنتحدث عن كيفية الوصول إلى البيانات عندما نصل إلى قوالب Blade. من الموجه إلى المتحكم يمكن أن نجعل الموجّه يؤشّر إلى المتحكم الذي يؤشّر بدوره إلى العرض، فالمتحكم هو أساسًا نسخة موسعة من دالة رد النداء، حيث سنتحدث عن المتحكمات بالتفصيل في المقال التالي. يخبرنا سطر الشيفرة البرمجية التالي أنه إذا استقبل الموجّه الوِجهة "‎/user"، فسيذهب لارافيل إلى الصنف UserController، ويستدعي التابع index: Route::get('/user', [UserController::class, 'index']); معاملات الوجهة تحتاج في بعض الأحيان إلى استخدام أجزاء من عنوان URL بوصفها معاملات، فمثلًا لنفترض أن لدينا مدونة مطورة بالكامل، ويوجد مستخدم يبحث عن منشور في المدونة له الاسم Slug الموجود في الرابط this-is-a-post، ويحاول العثور على هذا المنشور من خلال كتابة http://www.example.com/posts/this-is-a-post في متصفحه. يمكن التأكد من أن المستخدم قد عثر على المنشور الصحيح من خلال أخذ الجزء الموجود بعد posts/‎ كمعامل وإرساله إلى الواجهة الخلفية، ثم يمكن للمتحكم استخدام هذا المعامل للعثور على المنشور الصحيح وإعادته إلى المستخدم، حيث يمكنك تطبيق ذلك من خلال كتابة الشيفرة البرمجية التالية: Route::get('post/{slug}', [PostController::class, 'show']); سيتأكد السطر السابق من أن موجّه لارافيل يطابق الكلمة الموجودة بعد post/‎ كمعامل، ويسندها إلى المتغير slug عندما يرسلها لارافيل إلى الواجهة الخلفية. تُضمَّن معاملات الصنف Route دائمًا بين قوسين {} ويجب أن تتكون من أحرف أبجدية، ولا يجوز أن تحتوي على المحرف -. بما أننا لم نذكر المتحكمات في مثالنا، فيمكننا وضع دالة رد نداء بسيطة مكان المعامل الثاني واختبار الشيفرة البرمجية التالية: Route::get('/post/{slug}', function ($slug) { return $slug; }); افتح الآن متصفحك وانتقل إلى العنوان http://127.0.0.1:8000/posts/this-is-a-slug، وستظهر الصفحة التالية: يمكن أيضًا مطابقة معاملات متعددة في عنوان URL كما يلي: Route::get('category/{category}/post/{slug}', [PostController::class, 'show']); سيُسنَد الجزء الموجود بعد category/‎ إلى المتغير category في هذه الحالة، وسيُسنَد الجزء الموجود بعد post/‎ إلى المتغير slug. لا تعرف في بعض الأحيان ما إذا كان المعامل موجودًا في عنوان URL، وبالتالي يمكنك جعل هذا المعامل اختياريًا من خلال إلحاقه بإشارة استفهام (?) كما يلي: Route::get('post/{slug?}', [PostController::class, 'show']); وأخيرًا، يمكنك التحقق من صحة المعاملات باستخدام التعابير النمطية Regular Expressions، فمثلًا يمكنك التأكد من أن معرّف المستخدم هو عدد دائمًا كما يلي: Route::get('user/{id}', [UserController::class, 'show'])->where('id', '[0-9]+'); الموجهات المسماة تسمح الوِجهات المُسماة بإنشاء عناوين URL أو إعادة التوجيه بصورة مناسبة إلى وِجهات محددة، حيث يمكنك تحديد اسمٍ لوِجهة من خلال إضافة التابع name إلى سلسلة تعريف الوِجهة كما يلي: Route::get('user/profile', [UserController::class, 'show'])->name('profile'); وبالتالي إذا احتجت إلى الوصول إلى عنوان URL هذا، فيجب أن تستدعي الدالة route('profile')‎ فقط. تجميع الموجهات إذا أردت إنشاء موقع ويب كبير، فيجب أن يكون لديك عشرات أو حتى مئات من الموجّهات، وبالتالي من المنطقي أن تجمّعها مع بعضها بعضًا، فمثلًا يمكنك تجميعها بناءً على البرمجيات الوسيطة middleware كما يلي: Route::middleware(['auth'])->group(function () { Route::get('/user/profile', [UserController::class, 'show']); Route::get('/user/setting', [UserController::class, 'setting']); }); وستُسنَد الآن البرمجية الوسيطة auth إلى الموجّهَين. يمكنك أيضًا إسناد بادئات Prefixes إلى مجموعة من الموجّهات كما يلي: Route::prefix('admin')->group(function () { Route::get('/users', [UserController::class, 'show']); . . . }); سيكون لجميع الموجّهات المحدّدة في هذه المجموعة البادئة /admin/. البرمجيات الوسيطة Middleware البرمجية الوسيطة هي شيء يمكنه فحص وترشيح طلبات HTTP الواردة قبل أن تصل إلى تطبيقك، ويحدث بعد أن تطابق الوِجهة عنوان URL دون تنفيذ دالة رد النداء، وبالتالي هي شيء موجود في منطقة وسطى، ومن هنا جاءت تسميتها بالبرمجيات الوسيطة. يُعَد استيثاق المستخدم User Authentication من أمثلة البرمجيات الوسيطة، حيث إذا كان المستخدم مُستوثَقًا، فستأخذ الوِجهة هذا المستخدم إلى الهدف المفترض، وإن لم يكن الأمر كذلك، فستأخذ المستخدم إلى صفحة تسجيل الدخول أولًا. لن نكتب أيّ برمجيات وسيطة في هذه السلسلة من المقالات، ولكن البرمجية الوسيطة الوحيدة التي نحتاج إلى استخدامها هي البرمجية الوسيطة auth المُضمَّنة لأغراض الاستيثاق، والتي سنشرح أساسياتها. ننشئ برمجية وسيطة من خلال تشغيل الأمر التالي: php artisan make:middleware EnsureTokenIsValid مما سيؤدي إلى إنشاء صنف EnsureTokenIsValid جديد ضمن المجلد app/Http/Middleware. <?php namespace App\Http\Middleware; use Closure; class EnsureTokenIsValid { /** * معالجة الطلب الوارد * * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed */ public function handle($request, Closure $next) { if ($request->input('token') !== 'my-secret-token') { return redirect('home'); } return $next($request); } } ستحصل هذه البرمجيات الوسيطة على قيمة المفتاح Token من الطلب، وتقارنها مع المفتاح السري المُخزَّن على موقعنا، حيث إذا كان مطابقًا، فانتقل إلى الخطوة التالية، وإذا لم يكن كذلك، فأعِد التوجيه إلى الصفحة الرئيسية. يمكنك استخدام هذه البرمجية الوسيطة من خلال تسجيلها في لارافيل، لذا انتقل إلى الملف app/Http/Kernel.php، وابحث عن الخاصية ‎$routeMiddleware، وأدرِج البرمجية الوسيطة التي أنشأناها. /** * البرمجيات الوسيطة لوِجهات التطبيق * * يمكن إسناد هذه البرمجيات الوسيطة إلى مجموعات أو استخدامها بصورة فردية * * @var array<string, class-string|string> **/ protected $routeMiddleware = [ 'auth' => \App\Http\Middleware\Authenticate::class, 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 'can' => \Illuminate\Auth\Middleware\Authorize::class, 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, ]; وأخيرًا، تعرّفنا سابقًا على كيفية استخدام البرمجيات الوسيطة مع الموجّه كما يلي: Route::get('/profile', function () {. . .})->middleware('auth'); ترجمة -وبتصرُّف- للمقال Laravel for Beginners #1 - Getting Started لصاحبه Eric Hu. اقرأ أيضًا تعرف على إطار عمل تطوير الويب لارافيل Laravel مقارنة بين Django و Laravel و Rails تثبيت لارافيل وإعداده على Windows وUbuntu إنشاء مدونة باستخدام Laravel 5
  22. ناقشنا في مقالٍ سابق أن الرسوميات الحاسوبية ثلاثية الأبعاد تتكون من عمليتين: أولهما إنشاء عالم خيالي داخل الحاسوب، وثانيهما إنتاج صور ثنائية الأبعاد لذلك العالم من مجالات رؤية مختلفة؛ إذ يُعَد إنتاج صورة ثنائية الأبعاد من صورة ثلاثية الأبعاد مثالًا عن عملية الإسقاط Projection، وسنناقش في هذا المقال المفاهيم الأساسية فقط للإسقاط. سنوضّح في هذا المقال المواضيع التالية: تشكيل مستوٍ Plane واحد من شعاعين. مفهوم الإسقاط. إسقاط شعاعٍ على شعاع آخر. صيغة لإسقاط الأشعة. طريقة إسقاط الأشعة خطوةً بخطوة. المكون العمودي لإسقاط الأشعة. مثال لإسقاط الأشعة ثنائية الأبعاد. مثال لإسقاط الأشعة ثلاثية الأبعاد. المساقط ثنائية الأبعاد المتعددة يمكن إنتاج عدد لا نهائي من الصور ثنائية الأبعاد من نموذج واحد ثلاثي الأبعاد، فمثلًا، يعرض الشكل التالي إسقاط نموذج ثلاثي الأبعاد لصندوق على عرضٍ ثنائي الأبعاد؛ إذ تنتِج المواقع المختلفة لمجال الرؤية صورًا مختلفة ثنائية الأبعاد، وتُسقَط الأشعة ثلاثية الأبعاد للنموذج على سطح العرض لتكوين أشعة الصورة ثنائية الأبعاد. تشكيل مستوٍ واحد من شعاعين يمكن تشكيل مستوٍ واحد باستخدام شعاعين ونقطة دائمًا، إلا في حال كان هذان الشعاعان متوازيان. لا بد أنك تتذكر من مادة الهندسة في المرحلة الثانوية أن الخطين غير المتوازيين (ليسا على استقامة واحدة) يحددان مستوٍ واحد، وبالتالي ينطبق الشيء نفسه على الأشعة. تبدأ معظم الأمثلة في هذا المقال بشعاعين ثنائي الأبعاد، ولكن ستكون النتائج صالحة لشعاعين ثلاثي الأبعاد؛ إذ يحدّد شعاعان ثلاثي الأبعاد مستوٍ واحد أيضًا. يوضّح الشكل السابق الشعاع w والشعاع v، وجعلنا ذيل هذين الشعاعين يبدأ من النقطة نفسها لتسهيل التصور، ولكن لا تنسَ أن الأشعة ليس لها موضع محدّد، وجعلنا الشعاع v أفقيًا للسهولة أيضًا، ولكن يمكن أن يكون له أيّ منحى Orientation. إسقاط شعاع على شعاع آخر إذا ضربنا الشعاع v بعدد حقيقي Scalar (أو مقدار سلمي) k، فسيؤدي ذلك إلى تغيير طول هذا الشعاع، ولكنه لن يؤثر على منحاه، وبالتالي ليكن لدينا الشعاعان w و v كما في الشكل التالي: يمكن أن يكون الشعاعان w و v أيّ شعاعين، ونريد شعاعًا u عموديًا على الشعاع v وعددًا حقيقيًا k مضروبًا به، بحيث: w =‎ k v + u‎ ويسمى عندها k v‎ مسقط الشعاع w على الشعاع v. صيغة لكيفية إسقاط الأشعة ملاحظة: إذا كان الشعاعان u و v متعامدان، فإن الجداء النقطي لهما هو: ‎.u · v = 0 يجب أن تتحقق العلاقة: k v + u = w‎ بشرط أن يكون u عموديًا مع v، حتى يكون k v‎ مسقط الشعاع w على الشعاع v؛ إذ نعلم من علم المثلثات أن: طول المسقط k v‎ =‫ ‎| w | cos θ. منحى k v‎ هو شعاع الوحدة ‎vu‎. تذكر أن أشعة الوحدة Unit Vectors تُستخدَم لتمثيل المنحى في الفضاء ثلاثي الأبعاد، وبالتالي يكون منحى الشعاع v هو شعاع الوحدة ‎vu‎؛ إذ يبدو المنحى أفقيًا في الشكل السابق، ولكننا استخدمنا ذلك للسهولة فقط. وبما أن cos θ يساوي ‎wu · vu‎، فسينتج ما يلي: ‎k v = | w | (wu · vu) vu يمثل الجزء ‎| w | (wu · vu)‎ عددًا حقيقيًا بحيث يُضبَط الشعاع v على الطول المطلوب، وهو طول الخط الأفقي باللون الأزرق الفاتح في الشكل السابق؛ بينما يعبّر الجزء المتبقي من الصيغة: ‎vu‎ عن منحى الشعاع v نفسه. طريقة إسقاط الأشعة خطوة بخطوة تذكير: يمكنك حساب شعاع الوحدة من خلال تقسيم الشعاع على طوله. لنتّبع طريقة خطوة بخطوة لإسقاط الشعاع w على v، بدلًا من حفظ الصيغة السابقة لتطبيق عملية الإسقاط. أولًا، نحسب أطوال الأشعة كما يلي: ‎| w | = √(w · w) ‎| v | = √(v · v) ثانيًا، نحسب أشعة الوحدة: wu = w / | w‎ |‎ vu = | v | / | v |‎ ثالثًا، نحسب جيب تمام Cosine الزاوية بين الشعاعين: wu · vu رابعًا، نعوّض في صيغة الإسقاط التالية: ‎k v = | w | (wu · vu) vu المكون العمودي لإسقاط الأشعة ملاحظة: لا يُعَد مسقط الشعاع w على الشعاع v مماثلًا لمسقط الشعاع v على الشعاع w، إذ يكون منحى الشعاع الناتج عن عملية الإسقاط بالاتجاه نفسه للشعاع المُسقَط عليه. أوجدنا في الفقرة السابقة k v‎، لذا من السهل العثور على الشعاع المتعامد معه u: ‎w = k v + u‎، إذًا ‎u‎ = w - k v‎ تعبّر إشارة "+" في الصيغة السابقة عن جمع الأشعة، وتعبّر إشارة "-" عن طرح الأشعة. تدريبات عملية كانت الأشعة في هذا المقال جميعها أشعةً هندسيةً حتى الآن، إذ لم يكن هناك أيّ ذكرٍ للإطارات الإحداثية أو المصفوفات العمودية، ولكن ستنجح هذه النتائج عند تمثيل الأشعة بالمصفوفات العمودية كما سنرى في الأمثلة التالية. تدريب 1: نمثّل الشعاع w بالمصفوفة العمودية ‎(6, 5)T‎، ونمثّل الشعاع v بالمصفوفة العمودية ‎(9,0)T‎، ولنوجد مسقط الشعاع w على الشعاع v، أي لنبحث عن الشعاعين k v‎ و u. يمكنك ببساطة قراءة الإجابة من الرسم البياني الورقي التالي، ولكن تظاهر أنك لم تلاحظ ذلك. أولًا، نحسب الأطوال كما يلي: ‎| w | = √ ((6, 5)T‎·(6, 5)T‎) = 7.81‎ ‎| ‎v | = √ ((9, 0)T‎·(9, 0)T‎) = 9 ثانيًا، نحسب أشعة الوحدة كما يلي: ‎wu = (6, 5)T / 7.81 ‎vu = (9, 0)T / 9 = (1, 0)T ثالثًا، نحسب جيب تمام الزاوية بين الشعاعين وهو: ‎wu · vu = (1/7.81) (6, 5)(1, 0)T · (1, 0)(1, 0)T = 6/7.81 رابعًا، نعوّض في صيغة الإسقاط التالية: k v = | w | (wu · vu) vu ‎‎k v = 7.81 (6/7.81) (1, 0)T = 6(1, 0)T = (6, 0)T خامسًا، نحسب الشعاع العمودي: u = w - k v ‎u = (6, 5)T - (6, 0)T = (0, 5)T والنتيجة هي أن ‎u = (6,0)T + (0,5)T‎ كما هو متوقع. وكان هذا المثال سهلًا طبعًا. ملاحظة: ليس عليك أن تحسب طول w، لأنه يُلغَى قبل الإجابة النهائية، لذا فكل ما تحتاج إليه هو مربع طول v، أي ‎|v|2‎؛ إذ تعرض بعض الكتب صيغًا مختلفة للإسقاط تستفيد من هذه الحقائق. تدريب 2 إليك مثال آخر، ولكنه ليس بهذه السهولة، إذ نمثّل الشعاع w هنا بالمصفوفة العمودية ‎(3.2, 7)T‎، ونمثّل الشعاع v بالمصفوفة العمودية ‎(8, 4)T‎؛ ولنوجِد الآن الشعاعين k v‎ و u. أولًا، نحسب الأطوال كما يلي: | w | = (لا داعي لحسابه، لذا أبقيه بالرموز) ‎| v |2 = (8, 4)T ·(8, 4)T = 80 ثانيًا، نحسب أشعة الوحدة وهي: wu = (3.2, 7)T / | w |‎ vu = (8, 4)T / | v |‎ ثالثًا، نحسب جيب تمام الزاوية بين الشعاعين كما يلي: ‎wu · vu = (3.2, 7)T / | w | · (8, 4)T / | v | = 53.6 /( | w | | v |)‎ رابعًا، نعوّض في صيغة الإسقاط التالية: ‎k v = | w | (wu · vu) vu k v = | w | [53.6 / (| w | | v |)] (8, 4)T / | v |‎ k v = 53.6 / (| v |) (8, 4)T / | v |‎ ‎k v = 53.6 / (| v |2) (8, 4)T ‎k v = 53.6 / 80 (8, 4)T k v = ((53.6*8)/80, (53.6*4)/80)T = ( 5.3, 2.68)T خامسًا، نحسب الشعاع العمودي: u = w - k v u = (3.2, 7)T - (5.3, 2.68)T = (-2.1, 4.32)T لنتحقّق الآن من أن: ‎w = k v + u‎ من خلال تعويض النتائج في العلاقة k v + u‎ كما يلي: ‎( 5.3, 2.68)‎T + (-2.1, 4.32)T = (3.2, 7.0)T = w فوائد عملية إسقاط الأشعة يجب أن تفهم جيدًا ما يحدث عند إسقاط شعاع على شعاع آخر، إذ تحدث عملية الإسقاط -كما ذكرنا سابقًا- باستمرار عندما يعرض الحاسوب الرسوميات. تُنفَّذ معظم العمليات الحسابية في عتاد الرسوميات الخاصة بالحاسوب، ويجري إسقاط ملايين الأشعة في الثانية الواحدة؛ فإذا أردتَ أن تعرف ما يحدث في الرسوميات الحاسوبية، فيجب أن تعرف ما هي عملية الإسقاط. لنوجِد الآن مسقط الشعاع w على الشعاع v في الرسم البياني التالي: حاول إجراء العمليات الحسابية المطلوبة كما فعلنا في الأمثلة السابقة، ولكن يمكن أن تحصل على الإجابات التالية مباشرةً من قراءة الرسم البياني السابق: w = (4, 8.2)T v = (2.8, 1.75)T k v = (5.8, 1.6)T u = (-1.8, 6.4)T تبدو الإجابات السابقة جيدة، ولكن لنتحقّق منها كما يلي: أولًا، هل ‎w = k v + u‎؟ ‎(5.8, 1.6)T + (-1.8, 6.4)T = (4, 8.0)T = w (حسنًا، تقريبًا) ثانيًا، هل ‎v · u = 0.0؟ ‎(5.8, 1.6)T · (-1.8, 6.4)T = -10.44 + 10.24 = -0.2 = ‎0.0‎ (تقريبًا)‎ ملاحظة: لنفكر في جميع الأشعة المحتملة w و v ومسقط الشعاع w على الشعاع v، إذ يمكن أن يكون الشعاع k v‎ أطول من الشعاع v كما في المثال السابق، ولكن لا يمكن أن يكون الشعاع k v‎ أطول من الشعاع w، فأكبر طول ممكن للشعاع k v‎ هو | w | عندما يكون w و v على استقامة واحدة. مثال آخر لعملية الإسقاط لاحظ أن عملية إسقاط الشعاع w على v لا تعتمد على طول v، بل يؤثر اتجاه الشعاع v فقط على النتائج. يكون طول المسقط هو ‎| w | cos θ، حيث θ هي الزاوية بين الشعاعين، وبما أن دالة جيب التمام لا تزيد عن 1 أبدًا، فلن يكون طول المسقط أبدًا أطول من الشعاع المُسقَط. أسقِط الشعاع w على الشعاع v من خلال الاطلاع على الرسم البياني التالي: وستحصل على الإجابات التالية: w = (-5, 5)T v = (5, 1)T k v = (-3.8, -.8)T u = (-1.3, 5.6)T حصلنا على الإجابات السابقة من خلال قراءة الرسم البياني السابق، ولكنها ليست دقيقة بالضرورة، إذًا لنتحقّق منها. أولًا، هل ‎w = k v + u‎؟ ‎(-3.8, -0.8)T + (-1.3, 5.6)T = (-5.1, 4.8)T = w (تقريبًا) ثانيًا، هل ‎v · u = 0.0؟ ‎‎(5, 1)T · (-1.3, 5.6)T = -6.5 + 5.6 = -0.9 = 0.0 (ليس تمامًا) ملاحظة: يمكن لمسقط الشعاع w على الشعاع v أن يؤشّر إلى الاتجاه المعاكس للشعاع v كما حدث في المثال السابق. أمثلة لإسقاط الأشعة ثلاثية الأبعاد سنستعرض هنا بعض الأمثلة حول كيفية إسقاط الأشعة ثلاثية الأبعاد المثال الأول يظهر الرسم البياني السابق الشعاعين التاليين: ‎w = (4, 2.5, 2.5)T‎ (الشعاع الأحمر) ‎v = (5, 0, 3.1)T (الشعاع الأخضر) أسقِط الشعاع w على الشعاع v، وتذكّر أن طول الشعاع v لا يؤثر على الإسقاط، بل يؤثر منحاه عليه فقط، لذا أنزِل رأس سهم الشعاع w على الخط المنقط. يمكن بالنظر تقدير مسقط الشعاع ‎w = (4, 2.5, 2.5)T‎ على الشعاع ‎v = (5, 0, 3.1)T‎ بأنه k v = (4, 0, 2.5)T‎. يتضمن إسقاط الشعاع w على الشعاع v في هذه الحالة "انهيار Collapsing" البعد y للشعاع w. فلنتحقّق الآن مما إذا كانت النتيجة نفسها رياضيًا، إذ يمكن تطبيق الخطوات نفسها في الفضاء ثلاثي الأبعاد 3D. أولًا، نحسب الأطوال وهي: | w | = (لا داعي لحسابه، لذا أبقيه بالرموز) ‎| v |2 = ((5, 0, 3.1)T · (5, 0, 3.1)T) = 34.61 ثانيًا، نحسب أشعة الوحدة كما يلي: wu = (4, 2.5, 2.5)T / | w |‎ vu = (5, 0, 3.1)T / | v |‎ ثالثًا، نحسب جيب تمام الزاوية بين الشعاعين كما يلي: ‎wu · vu = (4, 2.5, 2.5)T / | w | · (5, 0, 3.1)T / | v | = 27.75 / ( | w | | v |) رابعًا، نعوّض في صيغة الإسقاط التالية: ‎k v = | w | (wu · vu) vu‎ k v = | w | (27.75 / (| w | | v |)) (5, 0, 3.1)T / | v |‎ k v = 27.75 / (| v |) (5, 0, 3.1)T / | v |‎ ‎k v = 27.75 / (| v |2) (5, 0, 3.1)T k v = (27.75 / 34.61) (5, 0, 3.1)T‎ ‎k v = ((27.75*5)/34.61, 0, (27.75*3.1)/34.61)T ‎k v = ( 4.00, 0, 2.49)T وبالتالي فإن التقدير بالنظر k v = (4, 0, 2.5)T‎ يطابق النتيجة الرياضية ‎(4, 0, 2.5)T‎، ويمكن الآن بسهولة حساب الشعاع العمودي: u = w - k v u = (4, 2.5, 2.5)T - (4, 0, 2.5)T = (0, 2.5, 0)T أبقِ الشعاع w كما هو في الرسم البياني السابق، وحاول تصوّر إسقاط w على شعاع مختلف v'‎ ينتج عنه المسقط k ،v'‎ الذي يؤدي إلى انهيار البعدين y و z للشعاع w؛ فمثلًا، إذا أسقطنا الشعاع ‎w = (4, 2.5, 2.5)T‎ على الشعاع ‎v' = (1, 0, 0)T‎، فستكون النتيجة k v = (4, 0, 0)T‎. المثال الثاني إذا أسقطنا شعاعًا في فضاء ثلاثي الأبعاد على محاور إحداثية، فستحصل على أحد الأبعاد الثلاثة فقط لذلك الشعاع، لذا لنلقِ نظرة على مثال آخر أصعب. لدينا من الشكل السابق الشعاعان ‎w = (4, 2, 3)T‎ و ‎v = (6, 4, 2)T‎، ونريد إسقاط الشعاع w على الشعاع v. يمكن أن نقدّر بالنظر أنه إذا "أنزلنا" رأس سهم الشعاع w عموديًا على الشعاع v، فستصل إلى الشعاع v عند حوالي 4/10 من طول v، لذا فإن k تساوي 0.4 تقريبًا و k v‎ يساوي: k v = 0.4 (6, 4, 2)T = (2.4, 1.6, 0.8)T ولكن سيتضح معنا أن هذه النتيجة التقديرية خاطئة بصورة فادحة، إذ يُعَد إيجاد الحل بالنظر في المسائل ثلاثية الأبعاد أمرًا صعبًا، إلّا إذا كانت لديك صورة ثلاثية الأبعاد فعلية أمامك. على هذا الأساس، دعنا نتحقق من الإجابة رياضيًا. أولًا، نحسب الأطوال وهي: | w | = (لا داعي لحسابه، لذا أبقيه بالرموز) ‎| v |2 = ((6, 4, 2)T · (6, 4, 2)T) = 56 ثانيًا، نحسب أشعة الوحدة كما يلي: wu = (4, 2, 3)T / | w |‎ vu = (6, 4, 2)T / | v |‎ ثالثًا، نحسب جيب تمام الزاوية بين الشعاعين كما يلي: wu · vu = (4, 2, 3)T · (6, 4, 2)T / | w | | v | = 38 / ( | w | | v |) رابعًا، نعوّض في صيغة الإسقاط التالية: k v = | w | (wu · vu) vu k v = | w | {38 / (| w | | v |)} {(6, 4, 2)T / | v |} k v = {38 / | v |} {(6, 4, 2)T / | v |} k v = {38 / | v |2} (6, 4, 2)T k v = {38/56} (6, 4, 2)T k v = 0.679 (6, 4, 2)T k v = ( 4.07, 2.7, 1.36)T لنحسب الآن الشعاع u كما يلي، حيث ‎u + k v = w‎ (الشعاع u هو الشعاع الأسود المنقط في الرسم البياني السابق): u + k v = w u = w - k v = (4, 2, 3)T - ( 4.07, 2.7, 1.36)T = (-0.07, -0.7, 1.64)T بهذا نكون قد وصلنا إلى نهاية هذا المقال الذي تعرّفنا فيه على كيفية إسقاط شعاع على شعاع آخر في فضاء ثنائي وثلاثي الأبعاد، وسنتعرّف في المقال التالي على عملية الجداء الشعاعي للأشعة. ترجمة -وبتصرُّف- للفصل Projecting one Vector onto Another من كتاب Vector Math for 3D Computer Graphics لصاحبه Bradley Kjell. اقرأ أيضًا المقال السابق: كيفية إيجاد الزاوية بين شعاعين في فضاء ثلاثي الأبعاد 3D تغيير حجم شعاع Scaling في التصاميم ثلاثية الأبعاد 3D تطبيق الجداء النقطي Dot Product على الأشعة في التصاميم 3D
  23. تستخدم لغة الاستعلام البنيوية Structured Query Language -أو SQL اختصارًا- مجموعةً متنوعة من هياكل البيانات، وتُعدّ الجداول من أكثرها استخدامًا، ولكن يكون لهذه الجداول قيود أو محدوديات معينة، فمثلًا لا يمكنك تقييد المستخدمين للوصول إلى جزء من الجدول فقط، إذ يجب أن يتمكّن المستخدم من الوصول إلى الجدول بأكمله، وليس إلى بعض الأعمدة ضمنه فقط. لنفترض مثلًا أنك تريد دمج البيانات من عدة جداول في هيكل بيانات جديد، ولكنك لا تريد حذف الجداول الأصلية، حيث يمكنك إنشاء جدول آخر فقط، ولكن سيكون لديك بيانات زائدة مخزَّنة في أماكن متعددة لاحقًا. قد يسبّب ذلك الكثير من الإزعاج، حيث إذا تغيرت بعض بياناتك، فيجب عليك أن تحدّثها في أماكن متعددة، لذا يمكن أن تكون العروض Views مفيدة في مثل هذه الحالات. يُعَد العرض view في لغة SQL جدولًا افتراضيًا، وتكون محتوياته نتيجة لاستعلام محدّد لجدول واحد أو أكثر، حيث تُعرَف هذه الجداول باسم الجداول الأساسية Base Tables. يقدّم هذا المقال نظرة عامة حول عروض SQL وفوائدها، ويوضّح كيفية إنشاء العروض والاستعلام عنها وتعديلها وتدميرها باستخدام صيغة SQL المعيارية. مستلزمات العمل يجب أن يكون لديك حاسوب يشغّل أحد أنواع أنظمة إدارة قواعد البيانات العلاقية Relational Database Management System -أو RDBMS اختصارًا- التي تستخدم لغة SQL. اختبرنا التعليمات والأمثلة الواردة في هذا المقال باستخدام البيئة التالية: خادم عامل على توزيعة أوبنتو Ubuntu مع مستخدم ذو صلاحيات مسؤول مختلف عن المستخدم الجذر وجدار حماية مضبوط باستخدام أداة UFW كما هو موضح في دليل الإعداد الأولي للخادم مع الإصدار 20.04 من أوبنتو، كما يمكنك الاطلاع على مقال كيفية تثبيت توزيعة أوبنتو من لينكس بأبسط طريقة. نظام MySQL مُثبَّت ومؤمَّن على الخادم كما هو موضح في مقال كيفية تثبيت MySQL على أوبنتو، وقد نفذنا الخطوات باستخدام مستخدم مُنشَأ وفق الطريقة الموضحة في الخطوة 3 من المقال. ملاحظة: تجدر الإشارة إلى أنّ الكثير من أنظمة إدارة قواعد البيانات العلاقية RDBMS لها تقديماتها الفريدة من لغة SQL، إذ ستعمل الأوامر المُقدمة في هذا المقال بنجاح مع معظم هذه الأنظمة، ولكن قد تجد بعض الاختلافات في الصيغة أو الناتج عند اختبارها على أنظمة مختلفة عن MySQL. ستحتاج أيضًا إلى قاعدة بيانات تحتوي على بعض الجداول المُحمَّلة ببيانات تجريبية نموذجية لتتمكن من التدرب على إنشاء العروض والتعامل معها، لذا اطّلع على القسم التالي للحصول على تفاصيل حول كيفية الاتصال بخادم MySQL وإنشاء قاعدة بيانات الاختبار المُستخدَمة في أمثلة هذا المقال. الاتصال بخادم MySQL وإعداد قاعدة بيانات تجريبية نموذجية إذا كان نظام قاعدة بيانات SQL الخاص بك يعمل على خادم بعيد، فاتصل بالخادم مُستخدمًا بروتوكول SSH من جهازك المحلي كما يلي: $ ssh user@your_server_ip ثم افتح واجهة سطر أوامر خادم MySQL مع وضع اسم حساب مستخدم MySQL الخاص بك مكان user: $ mysql -u user -p أنشِئ قاعدة بيانات باسم views_db في موجّه الأوامر: mysql> CREATE DATABASE views_db; إذا أُنشئِت قاعدة البيانات بنجاح، فسيظهر خرج كما يلي: الخرج Query OK, 1 row affected (0.01 sec) يمكنك اختيار قاعدة البيانات views_db من خلال تنفيذ تعليمة USE التالية: mysql> USE views_db; ويكون الخرج كما يلي: الخرج Database changed اخترنا قاعدة بيانات views_db، وسننشئ جدولين ضمنها. لنفترض مثلًا أنك تدير خدمة رعاية للكلاب، فقررت استخدام قاعدة بيانات SQL لتخزين معلومات حول كل كلب مُسجَّل في الخدمة، بالإضافة إلى معلومات كل متخصص في رعاية الكلاب توظفه خدمتك، حيث نظّمت هذه المعلومات من خلال إنشاء جدولين: أحدهما يمثل الموظفين والآخر يمثل الكلاب التي تعتني بها خدمتك. سيحتوي الجدول الذي يمثل موظفيك على الأعمدة التالية: emp_id: رقم تعريف لكل موظف يقدّم رعايةً للكلاب، ونعبّر عنه باستخدام نوع البيانات int، وسيكون هذا العمود هو المفتاح الرئيسي Primary Key للجدول، أي أن كل قيمة ستمثّل معرّفًا فريدًا للصف الخاص بها، وسيكون لهذا العمود أيضًا قيد UNIQUE مطبَّق عليه، إذ يجب أن تكون كل قيمة في المفتاح الرئيسي فريدة. emp_name: اسم الموظف، ونعبّر عنه باستخدام نوع البيانات varchar بحد أقصى 20 محرفًا. نفّذ تعليمة CREATE TABLE التالية لإنشاء جدول بالاسم employees ويحتوي على العمودين التاليين: mysql> CREATE TABLE employees ( mysql> emp_id int UNIQUE, mysql> emp_name varchar(20), mysql> PRIMARY KEY (emp_id) mysql> ); سيحتوي الجدول الآخر الذي يمثل الكلاب على الأعمدة الستة التالية: dog_id: رقم تعريف لكل كلب ويُعبَّر عنه بنوع البيانات int، وسيكون هذا العمود هو المفتاح الرئيسي للجدول dogs مثل العمود emp_id في الجدول employees. dog_name: اسم الكلب ويُعبَّر عنه باستخدام نوع البيانات varchar بحد أقصى 20 محرفًا. walker: يخزّن هذا العمود رقم معرّف الموظف الذي يرعى كل كلب. walk_distance: المسافة التي يجب أن يمشيها كل كلب عند اصطحابه للتمرين، ويُعبَّر عنه باستخدام نوع البيانات decimal، ويمكن أن تحتوي القيم في هذا العمود على ثلاثة أرقام على الأكثر مع وجود رقمين من هذه الأرقام على يمين الفاصلة العشرية. meals_perday: توفر هذه الخدمة لكل كلب عددًا معينًا من الوجبات كل يوم، حيث يحتوي هذا العمود على عدد الوجبات التي يجب أن يحصل عليها كل كلب يوميًا حسب طلب مالكه، ويستخدم نوع البيانات int فهو عدد صحيح. cups_permeal: يمثل هذا العمود عدد أكواب الطعام التي يجب أن يحصل عليها كل كلب في كل وجبة، ويُعبَّر عنه بنوع البيانات decimal مثل العمود walk_distance، ويمكن أن تحتوي القيم في هذا العمود على ما يصل إلى ثلاثة أرقام مع وجود رقمين من هذه الأرقام على يمين الفاصلة العشرية. نتأكد من أن العمود walker يحتوي على القيم التي تمثل أرقام معرّف الموظف الصالحة فقط من خلال تطبيق قيد مفتاح خارجي Foreign Key على العمود walker الذي يشير إلى العمود emp_ID الخاص بالجدول employees. يُعَد قيد المفتاح الخارجي طريقة للتعبير عن العلاقة بين جدولين من خلال اشتراط أن تكون القيم الموجودة في العمود الذي طُبِّق المفتاح الخارجي عليه موجودة في العمود الذي يشير إليه، حيث يشترط قيد FOREIGN KEY في المثال التالي أن تكون أيّ قيمة مضافة إلى العمود walker في الجدول dogs موجودةً في العمود emp_ID الخاص بالجدول employees. لننشئ جدولًا بالاسم dogs يحتوي على هذه الأعمدة باستخدام الأمر التالي: mysql> CREATE TABLE dogs ( mysql> dog_id int UNIQUE, mysql> dog_name varchar(20), mysql> walker int, mysql> walk_distance decimal(3,2), mysql> meals_perday int, mysql> cups_permeal decimal(3,2), mysql> PRIMARY KEY (dog_id), mysql> FOREIGN KEY (walker) mysql> REFERENCES employees(emp_ID) ); يمكنك الآن تحميل الجدولين ببعض البيانات التجريبية النموذجية. نفّذ عملية الإدخال INSERT INTO التالية لإضافة ثلاثة صفوف من البيانات تمثّل ثلاثة من موظفي الخدمة إلى الجدول employees: mysql> INSERT INTO employees mysql> VALUES mysql> (1, 'Peter'), mysql> (2, 'Paul'), mysql> (3, 'Mary'); ثم نفّذ العملية التالية لإدخال سبعة صفوف من البيانات في الجدول dogs: mysql> INSERT INTO dogs mysql> VALUES mysql> (1, 'Dottie', 1, 5, 3, 1), mysql> (2, 'Bronx', 3, 6.5, 3, 1.25), mysql> (3, 'Harlem', 3, 1.25, 2, 0.25), mysql> (4, 'Link', 2, 2.75, 2, 0.75), mysql> (5, 'Otto', 1, 4.5, 3, 2), mysql> (6, 'Juno', 1, 4.5, 3, 2), mysql> (7, 'Zephyr', 3, 3, 2, 1.5); وأصبحتَ الآن جاهزًا لمتابعة بقية هذا المقال والبدء في تعلّم كيفية استخدام العروض في لغة SQL. فهم وإنشاء العروض Views يمكن أن تصبح استعلامات SQL معقدة، ولكن إحدى الفوائد الرئيسية للغة SQL هي أنها تتضمن العديد من الخيارات والتعليمات التي تسمح لك بترشيح بياناتك بمستوى عالٍ من الدقة والتحديد. إذا كانت لديك استعلامات معقدة تريد تشغيلها بصورة متكررة، فقد يصبح الاضطرار إلى كتابتها باستمرار أمرًا مملًا، وإحدى الطرق لحل هذه المشكلات هي استخدام العروض. تُعَد العروض views جداولًا افتراضية كما ذكرنا سابقًا، وهذا يعني أنها تشبه الجداول وظيفيًا، ولكنها تمثّل نوعًا مختلفًا من هياكل البيانات لأن العرض لا يحتوي على أيّ بيانات خاصة به، فهو يسحب البيانات من جدول أساسي واحد أو أكثر، وتكون المعلومات الوحيدة حول العرض التي سيخزنها نظام إدارة قواعد البيانات DBMS هي هيكل العرض. تُسمَّى العروض أحيانًا بالاستعلامات المحفوظة Saved Queries، لأنها تمثّل الاستعلامات المحفوظة باسم مُحدَّد لتسهيل الوصول إليها. ليكن لدينا المثال التالي، تخيل أن أعمال رعاية الكلاب الخاصة بك ناجحة وتريد طباعة جدول يومي لجميع موظفيك، إذ يجب أن يحتوي هذا الجدول على كل كلب ترعاه الخدمة، والموظف المكلف برعايته، والمسافة التي يجب أن يمشيها كل كلب يوميًا، وعدد الوجبات التي يجب إطعامها لكل كلب يوميًا، وكمية الطعام التي ينبغي أن يحصل كل كلب في كل وجبة. استخدم مهاراتك في لغة SQL لإنشاء استعلام مع بياناتٍ تجريبية نموذجية من الخطوة السابقة لاسترجاع جميع هذه المعلومات للجدول، ولاحظ أن هذا الاستعلام يتضمن صيغة JOIN لسحب البيانات من الجدولين employees و dogs: mysql> SELECT emp_name, dog_name, walk_distance, meals_perday, cups_permeal mysql> FROM employees JOIN dogs ON emp_ID = walker; الخرج +----------+----------+---------------+--------------+--------------+ | emp_name | dog_name | walk_distance | meals_perday | cups_permeal | +----------+----------+---------------+--------------+--------------+ | Peter | Dottie | 5.00 | 3 | 1.00 | | Peter | Otto | 4.50 | 3 | 2.00 | | Peter | Juno | 4.50 | 3 | 2.00 | | Paul | Link | 2.75 | 2 | 0.75 | | Mary | Bronx | 6.50 | 3 | 1.25 | | Mary | Harlem | 1.25 | 2 | 0.25 | | Mary | Zephyr | 3.00 | 2 | 1.50 | +----------+----------+---------------+--------------+--------------+ 7 rows in set (0.00 sec) لنفترض أنه يجب تنفيذ هذا الاستعلام بشكل متكرر، إذ قد يصبح أمرًا مملًا إذا اضطررت إلى كتابة الاستعلام مرارًا وتكرارًا، وخاصةً عند إجراء تعليمات استعلام أطول وأكثر تعقيدًا، وإذا أردتَ إجراء تعديلات طفيفة على الاستعلام أو التوسّع فيه، فقد يكون الأمر مملًا عند استكشاف الأخطاء وإصلاحها مع وجود العديد من الاحتمالات للأخطاء الصياغية. يمكن أن يكون العرض مفيدًا في مثل هذه الحالات، لأنه يُعَد جدولًا مشتقًا من نتائج الاستعلام. تستخدم معظم أنظمة RDBMS الصيغة التالية لإنشاء العروض: CREATE VIEW view_name AS SELECT statement; يمكنك بعد تعليمة CREATE VIEW اختيار اسمٍ للعرض الذي ستستخدمه للإشارة إليه لاحقًا، ثم تدخِل الكلمة المفتاحية AS، ثم تضع استعلام SELECT الذي تريد حفظ خرجه. يمكن أن يكون الاستعلام الذي تستخدمه لإنشاء عرضك أيّ تعليمة SELECT صالحة، ويمكن أن تستعلم التعليمة التي تضمّنها عن جدول أساسي واحد أو أكثر طالما أنك تستخدم الصيغة الصحيحة. جرّب إنشاء عرض باستخدام استعلام المثال السابق، حيث تسمّي عملية CREATE VIEW العرض بالاسم walking_schedule: mysql> CREATE VIEW walking_schedule mysql> AS mysql> SELECT emp_name, dog_name, walk_distance, meals_perday, cups_permeal mysql> FROM employees JOIN dogs mysql> ON emp_ID = walker; ستتمكّن بعد ذلك من استخدام هذا العرض والتفاعل معه كما تفعل مع أيّ جدول آخر، فمثلًا يمكنك تنفيذ الاستعلام التالي لإعادة جميع البيانات الموجودة في العرض: mysql> SELECT * FROM walking_schedule; الخرج +----------+----------+---------------+--------------+--------------+ | emp_name | dog_name | walk_distance | meals_perday | cups_permeal | +----------+----------+---------------+--------------+--------------+ | Peter | Dottie | 5.00 | 3 | 1.00 | | Peter | Otto | 4.50 | 3 | 2.00 | | Peter | Juno | 4.50 | 3 | 2.00 | | Paul | Link | 2.75 | 2 | 0.75 | | Mary | Bronx | 6.50 | 3 | 1.25 | | Mary | Harlem | 1.25 | 2 | 0.25 | | Mary | Zephyr | 3.00 | 2 | 1.50 | +----------+----------+---------------+--------------+--------------+ 7 rows in set (0.00 sec) يُعَد هذا العرض مشتقًا من جدولين آخرين، ولكنك لن تتمكّن من الاستعلام عن العرض لأيّ بيانات من هذين الجدولين إن لم تكن موجودة مسبقًا في هذا العرض. يحاول الاستعلام التالي استرجاع العمود walker من العرض walking_schedule، ولكن سيفشل هذا الاستعلام لأن العرض لا يحتوي على أيّ أعمدة بهذا الاسم: mysql> SELECT walker FROM walking_schedule; الخرج ERROR 1054 (42S22): Unknown column 'walker' in 'field list' يعيد هذا الخرج رسالة خطأ لأن العمود walker هو جزء من الجدول dogs، ولكنه غير مُضمَّنٍ في العرض الذي أنشأناه. يمكنك أيضًا تنفيذ الاستعلامات التي تتضمّن دوالًا تجميعية Aggregate Functions تعالج البيانات ضمن العرض، يستخدم المثال التالي الدالة التجميعية MAX مع عبارة GROUP BY للعثور على أطول مسافة يجب على الموظف أن يمشيها في يوم محدّد: mysql> SELECT emp_name, MAX(walk_distance) AS longest_walks mysql> FROM walking_schedule GROUP BY emp_name; الخرج +----------+---------------+ | emp_name | longest_walks | +----------+---------------+ | Peter | 5.00 | | Paul | 2.75 | | Mary | 6.50 | +----------+---------------+ 3 rows in set (0.00 sec) توجد فائدة أخرى للعروض كما ذكرنا سابقًا، وهي أنه يمكنك استخدامها لتقييد وصول مستخدم قاعدة البيانات إلى العرض فقط بدلًا من الوصول إلى الجدول أو قاعدة البيانات بأكملها. لنفترض مثلًا أنك وظّفتَ مدير مكتب لمساعدتك في إدارة الجدول الزمني، وتريد أن يصل إلى معلومات الجدول دون الوصول لأي بيانات أخرى في قاعدة البيانات، حيث يمكنك إنشاء حساب مستخدم جديد له في قاعدة بياناتك كما يلي: mysql> CREATE USER 'office_mgr'@'localhost' IDENTIFIED BY 'password'; يمكنك بعد ذلك منح هذا المستخدم الجديد صلاحية وصول للقراءة إلى العرض walking_schedule فقط باستخدام التعليمة GRANT كما يلي: mysql> GRANT SELECT ON views_db.walking_schedule to 'office_mgr'@'localhost'; وبالتالي سيتمكّن الشخص الذي لديه صلاحية الوصول إلى حساب مستخدم MySQL الذي هو office_mgr من تنفيذ استعلامات SELECT في العرض walking_schedule فقط. تغيير وحذف العروض Views إذا أضفتَ أو غيّرتَ بياناتٍ في أحد الجداول التي نشتق العرض منها، فستُضاف أو تُحدَّث البيانات ذات الصلة في العرض تلقائيًا. نفّذ الأمر INSERT INTO التالي لإضافة صف آخر إلى الجدول dogs: mysql> INSERT INTO dogs VALUES (8, 'Charlie', 2, 3.5, 3, 1); يمكنك بعد ذلك استرجاع جميع البيانات من العرض walking_schedule مرة أخرى كما يلي: mysql> SELECT * FROM walking_schedule; الخرج +----------+----------+---------------+--------------+--------------+ | emp_name | dog_name | walk_distance | meals_perday | cups_permeal | +----------+----------+---------------+--------------+--------------+ | Peter | Dottie | 5.00 | 3 | 1.00 | | Peter | Otto | 4.50 | 3 | 2.00 | | Peter | Juno | 4.50 | 3 | 2.00 | | Paul | Link | 2.75 | 2 | 0.75 | | Paul | Charlie | 3.50 | 3 | 1.00 | | Mary | Bronx | 6.50 | 3 | 1.25 | | Mary | Harlem | 1.25 | 2 | 0.25 | | Mary | Zephyr | 3.00 | 2 | 1.50 | +----------+----------+---------------+--------------+--------------+ 8 rows in set (0.00 sec) لاحظ وجود صف آخر في مجموعة نتائج الاستعلام، والذي يمثّل البيانات التي أضفتها إلى الجدول dogs، ولكن لا يزال العرض يسحب البيانات ذاتها من الجداول الأساسية نفسها، لذلك لم تغيّر هذه العملية العرض. تسمح لك العديد من أنظمة RDBMS بتحديث هيكل العرض بعد إنشائه باستخدام صيغة CREATE OR REPLACE VIEW: mysql> CREATE OR REPLACE VIEW view_name mysql> AS mysql> new SELECT statement إذا كان العرض الذي اسمه view_name موجودًا مسبقًا في هذه الصيغة، فسيحدّث نظام قاعدة البيانات هذا العرض بحيث يمثّل البيانات التي تعيدها التعليمة new SELECT statement. إذا لم يكن العرض بهذا الاسم موجودًا، فسينشئ نظام إدارة قواعد البيانات DBMS عرضًا جديدًا. لنفترض أنك تريد تغيير العرض walking_schedule ليسرد إجمالي كمية الطعام التي تناولها كل كلب على مدار اليوم بدلًا من سرد عدد أكواب الطعام التي يتناولها كل كلب في كل وجبة، ويمكنك تغيير العرض باستخدام الأمر التالي: mysql> CREATE OR REPLACE VIEW walking_schedule mysql> AS mysql> SELECT emp_name, dog_name, walk_distance, meals_perday, (cups_permeal * mysql> meals_perday) AS total_kibble mysql> FROM employees JOIN dogs ON emp_ID = walker; إذا أجربتَ الآن استعلامًا على هذا العرض، فستمثّل مجموعة النتائج بيانات العرض الجديدة كما يلي: mysql> SELECT * FROM walking_schedule; الخرج +----------+----------+---------------+--------------+--------------+ | emp_name | dog_name | walk_distance | meals_perday | total_kibble | +----------+----------+---------------+--------------+--------------+ | Peter | Dottie | 5.00 | 3 | 3.00 | | Peter | Otto | 4.50 | 3 | 6.00 | | Peter | Juno | 4.50 | 3 | 6.00 | | Paul | Link | 2.75 | 2 | 1.50 | | Paul | Charlie | 3.50 | 3 | 3.00 | | Mary | Bronx | 6.50 | 3 | 3.75 | | Mary | Harlem | 1.25 | 2 | 0.50 | | Mary | Zephyr | 3.00 | 2 | 3.00 | +----------+----------+---------------+--------------+--------------+ 8 rows in set (0.00 sec) يمكنك حذف العروض باستخدام صيغة DROP مثل معظم هياكل البيانات الأخرى التي يمكنك إنشاؤها في لغة SQL، وإليك مثالًا: DROP VIEW view_name; يمكنك مثلًا حذف العرض walking_schedule باستخدام الأمر التالي: mysql> DROP VIEW walking_schedule; يؤدي الأمر السابق إلى إزالة العرض walking_schedule من قاعدة بياناتك، ولكنه لن يحذف أيًّا من بيانات قاعدة بياناتك المتعلقة بالعرض إلّا إذا أزلتها من الجداول الأساسية. الخلاصة تعلّمنا في هذا المقال ما هي عروض SQL وكيفية إنشائها والاستعلام عنها وتغييرها وحذفها من قاعدة البيانات، وتعرّفنا على فوائد العروض، وأنشأنا مستخدم MySQL الذي يمكنه فقط قراءة البيانات من العرض التجريبي الذي أنشأناه. يجب أن تعمل الأوامر الواردة في أمثلة هذا المقال على معظم قواعد البيانات العلاقية، ولكن يجب أن تدرك أن كل قاعدة بيانات SQL لها تقديمها الفريد من هذه اللغة، لذا يجب عليك الرجوع إلى التوثيق الرسمي لنظام إدارة قواعد البيانات DBMS الخاص بك للحصول على وصف أشمل لكل أمر ومجموعته الكاملة من الخيارات. ننصحك بالاطلاع على سلسلة تعلم SQL في أكاديمية حسوب للمزيد حول كيفية التعامل مع لغة SQL. ترجمة -وبتصرف- للمقال How To Use Views in SQL لصاحبه Mark Drake. اقرأ أيضًا الاستعلام عن البيانات في SQL جلب الاستعلامات عبر SELECT في SQL المرجع المتقدم إلى لغة SQL لغة معالجة البيانات DML الخاصة بلغة SQL
  24. رأينا في المقالات السابقة أن نستطيع استخدام الجداء النقطي Dot Product من أجل الآتي: حساب طول الشعاع: حيث يكون الجداء النقطي للشعاع مع نفسه = الطول2. اكتشاف التعامد بين شعاعين: حيث يكون الجداء النقطي لشعاعين متعامدين = 0. حساب الزاوية بين شعاعين: حيث يكون cosθ = حاصل الجداء النقطي لشعاعي وحدة Unit Vectors. لن نضيف في هذا المقال شيئًا جديدًا عمّا تعلمناه في المقال السابق، ولكننا سنوسّع مفهوم حساب الزاوية بين شعاعين ليشمل حساب الزاوية بين شعاعين في فضاء ثلاثي الأبعاد. سنتعرّف في هذا المقال على المواضيع الآتية: صيغة حساب جيب تمام Cosine الزاوية بين أشعة الوحدة. قائمة بالخطوات التي يجب اتخاذها لتطبيق هذه الصيغة على الأشعة التي ليست أشعة وحدة. استخدام الأشعة في مسائل الهندسة الفراغية. الزوايا في الفضاء ثلاثي الأبعاد تقع أي حافتين من الحواف الثلاثة المشكّلة لزاوية في صندوق من الورق المقوى في مستوٍ واحد، وتساوي الزاوية بينهما 90 درجة، إذ تتقاطع جميع حواف الصندوق بزوايا قائمة، ولكن لن تساوي الزوايا بين الحواف 90 درجة عادةً عند إسقاط الحواف لتكوين صورة ثنائية الأبعاد. الزوايا بين محاور الإحداثيات لا يؤدي تغيير مجال رؤيتك إلى تغيير الكائنات ثلاثية الأبعاد، ولكن ستتغيّر الصورة ثنائية الأبعاد. تعتمد الزاوية بين شعاعين في الفضاء ثلاثي الأبعاد على علاقتهما ببعضهما البعض، ولا تعتمد على مجال رؤيتك أو إطار الإحداثيات الذي تستخدمه؛ ولكن زوايا الصورة تعتمد كثيرًا على مجال الرؤية عندما تعرض صورة ثنائية الأبعاد مشهدًا ثلاثي الأبعاد. رأينا في المقال السابق الصيغة التالية لحساب الزاوية θ بين شعاعي الوحدة: au · bu = cosθ‎ إذًا لنطبّق هذه الصيغة على شعاعي الوحدة الموازيين للمحور x والمحور y وهما: ‎(1,0,0)T‎ و ‎(0,1,0)T‎ لحساب الزاوية المحصورة بين المحورين x و y كما يلي: ‏‏‎(1,0,0)T ‎· (0,1,0)T =‎ 0 وبالتالي فإن المحورين x و y متعامدان كما توقعنا. الزاوية بين شعاعين كان المثال السابق سهل التصور والحساب، إذًا لنجرب شيئًا أصعب، حيث يظهر الشكل الآتي شعاعين: الشعاع a الذي تمثله المصفوفة العمودية ‎(4,4,4)T‎. الشعاع b الذي تمثله المصفوفة العمودية ‎(4,0,4)T‎. لاحظ أن الزاوية بين الشعاعين لا تساوي 90 درجة، وبالتالي يصعب رؤيتها مقارنةً بالمثال السابق. يمكن أن نخمّن بصريًا إلى حدٍ ما أن الزاوية بين الشعاعين a و b أقل من منتصف المسافة بين المستوي العمودي والأفقي، إذ تكون الزاوية التي تساوي 35 درجة تخمينًا جيدًا، ولكن التخمين غير كافٍ ونحتاج صيغةً لحساب الزاوية، وهذا ما سنوضّحه في الفقرة التالية. صيغة حساب الزاوية بين شعاعين لاحظ أنه عند الانتقال من ذيل الشعاع a إلى رأسه تزيد المسافة العمودية بمقدار 4، بينما تزيد المسافة الأفقية بمقدار 4‎ √2، وظل الزاوية Tangent هو: 4‎ / (4 √2) = 1.0/ √2 = 0.7071 وبالتالي فإن الزاوية مع المستوي الأفقي هي: arctan( 0.7071 )‎ =‫ 35.26 درجة. وبما أن الشعاع b يقع في المستوي الأفقي، فإن الزاوية بين الشعاعين يجب أن تساوي هذه القيمة. وبالتالي فإن صيغة حساب الزاوية θ بين شعاعي وحدة هي: au · bu = cosθ ويمكننا استخدام هذه الصيغة مع الأشعة التي ليست أشعة وحدة باتباع الخطوات التالية: توحيد Normalize كل شعاع. حساب الجداء النقطي لشعاعي الوحدة الناتجين. استخدم arc cos للحصول على الزاوية. لنطبّق هذه الصيغة على الشعاع a الذي تمثله ‎(4,4,4)T‎ والشعاع b الذي تمثله ‎(4,0,4)T‎ كما يلي: ‎| a | = √(16 + 16 + 16) = 4√3 و | b | = √(16 + 16) = 4√2 au = (4, 4, 4)T / (4√3)‎ و au = (4, 0, 4)T / (4 √2 ) au · bu = (16 + 16)/( (4√3)(4 √2) ) = 2/(√3√2 ) = √2 /√3 = cosθ‎ cosθ = 0.81649 وبالتالي فإن الزاوية بين الشعاعين هي: θ =‫ 35.26 درجة. لا تُعَد الحسابات الكثيرة في التمرين السابق هي الغرض الحقيقي، ولكن الهدف هو توضيح الصيغة ‎au · bu = cosθ، والتي تُعَد مهمةً في كل جزء من أجزاء الرسوميات ثلاثية الأبعاد. تدريب عملي يوضح الشكل الآتي شعاعين تمثّلهما المصفوفتان العموديتان التاليتان: f = (2, 4, 6)T‎ ‎g = (6, 4, 3)T يمكن أن ترغب في قياس الزاوية بين الشعاعين من خلال وضع مِنقلة مسطحة بين الشعاعين، ولكن لا يمكنك ذلك لأن كل ما تراه هو إسقاطٌ للشعاعين على الشاشة. يمكننا تخمين الزاوية التي تفصل بين الشعاعين بمقدار 30 درجة، ولكنه مجرد تخمين، إذًا لنطبّق الصيغة لمعرفة مدى صحة هذا التخمين. أولًا، نحسب الأطوال وهي: ‎| f |2 = (2, 4, 6)T · (2, 4, 6)T = 4 + 16 + 36 = 56 ‎| g |2 = (6, 4, 3)T · (6, 4, 3)T = 36 + 16 + 9 = 61 ثانيًا، نوحّد الشعاعين كما يلي: fu = (2, 4, 6)T / √56‎ gu = (6, 4, 3)T / √61‎ ثالثًا، نحسب الجداء النقطي لهما وهو: fu · gu = (2, 4, 6)T · (6, 4, 3)T / ( √56 √61) = (12+16+18)/( √56 √61) = 46 / ( √56 √61) = 0.78704‎ رابعًا، نحسب الزاوية بينهما كما يلي: cos θ = 0.78704‎ θ = arc cos 0.78704 = 38.1‎ درجة لاحظ أن الجواب ليس بعيدًا جدًا عن التخمين. الزاوية بين اتجاهي شعاعين ليس من الضروري أن يتلامس الشعاعان عند ذيليهما حتى تكون هناك زاوية بينهما، فليس للأشعة موضع محدد، إذ يمكنك أخذ الجداء النقطي لأي شعاعين ثلاثي الأبعاد أو المصفوفات العمودية التي تمثلها. قد يبدو من الغريب قياس الزاوية بين الأشياء التي ليس لها موضع محدد، ولكن العديد من الظواهر لها منحًى Orientation، دون أن يكون لها موضع محدد؛ إذ قد تأتي الرياح من الغرب في أحد الأيام، ومن الشمال في اليوم التالي (بفارق 90 درجة)؛ وقد يصل ضوء الشمس عند الظهيرة في الصيف بزاوية مختلفة عن زاوية وصوله عند الظهيرة في الشتاء. يستخدم التظليل في الرسوميات ثلاثية الأبعاد أشعةً لمنحى السطح، وأشعة أخرى لاتجاه كل مصدر للضوء. ليكن باسم وبلال جالسين على الشاطئ، ويريدان تسمير بشرتهما كما في الشكل التالي: يؤشر إطار الإحداثيات المناسب إلى المحور x شمالًا، والمحور y للأعلى بصورة مستقيمة والمحور z شرقًا، حيث يكون الاتجاه نحو الشمس في هذا الإطار هو ‎(-3, 4, 0)T / 5، وشعاع الوحدة العمودي على ظهر باسم هو ‎( -1, 2, 2)T / 3، وشعاع الوحدة العمودي على ظهر بلال هو ‎( -2, 1, 2)T / 3. إذًا سيكون أحدهما متمتعًا بأفضل موضع للتسمير السريع، حيث سنوضّح ذلك كما يلي: الاتجاه نحو الشمس = ‎( -3, 4, 0)/5. الاتجاه العمودي على ظهر باسم = ‎( -1, 2, 2)/3. الاتجاه العمودي على ظهر بلال = ‎( -2, 1, 2)/3. يجب أن يكون ظهرك مُوجَّهًا مباشرةً نحو الشمس ليصل إليك أكبر قدرٍ من أشعتها، كما يجب أن تكون الزاوية بين الاتجاه العمودي على ظهرك واتجاه الشمس صغرى. بالنسبة لباسم: ‎( -1, 2, 2)/3 · ( -3, 4, 0)/5 = (3+8+0)/15 = 11/15 arc cos( 11/15 ) = 42.8‎ درجة وبالنسبة لبلال: ‎( -2, 1, 2)/3 · ( -3, 4, 0)/5 = (6+4+0)/15 = 10/15 arc cos( 14/15 ) = 48.2 درجة وبالتالي سيحصل باسم على تسمير لبشرته بصورة أسرع. تدريبات عملية تدريب 1: يظهر الرسم البياني الآتي الشعاعين التاليين: q = (-2, 4, 3)T p = (3, 1, -4)T وضعنا ذيل الأشعة عند نقطة الأصل لسهولة التصور، إذ تؤشّر الأشعة إلى ثُمنَين Octants مختلفتين من الفضاء ثلاثي الأبعاد. لنحسب الآن الزاوية بين هذين الشعاعين كما يلي: أولًا، نحسب الأطوال وهي: ‎| p |2 = ( -2, 4, 3)T · ( -2, 4, 3)T = 4 + 16 + 9 = 29 ‎| q |2 = ( 3, 1, -4)T · ( 3, 1, -4)T = 9 + 1 + 16 = 26 ثانيًا، نوحّد الشعاعين كما يلي: pu = (-2, 4, 3)T / √29‎ qu = (3, 1, -4)T / √26‎ ثالثًا، نحسب الجداء النقطي لهما وهو: pu · qu = (-2, 4, 3)·(3, 1, -4)T / ( √29 √26) = (-6 + 4 - 12)/( √29 √26) = -14/( √29 √26) = -0.50985‎ رابعًا، نحسب الزاوية بينهما كما يلي: cos θ = -0.50985 θ = arc cos( -0.50985) = 120.654‎ درجة تدريب2: أوجد جيب تمام الزاوية بين شعاعي الوحدة التاليين: s = (1, 0, 1)T /√2‎ t = (1, 1, 1)T /√3‎ cosθ = (1, 0, 1)T · (1, 1, 1)T / ( √2 √3) = 2 / ( √2 √3) = √2 / √3 = 0.8164‎ استخدام صيغة الجداء النقطي لحل المسائل الهندسية يمكن استخدام صيغة الجداء النقطي لحل بعض المسائل الهندسية التي قد تكون صعبة الحل، فمثلًا، تتطابق الحواف الحمراء في الشكل التالي على محاور الإحداثيات، وتشترك في نقطة النهاية (0, 0, 0)، وتنتهي الحافة على طول المحور x عند x=2، وتنتهي الحافة على طول المحور y عند y=3، وتنتهي الحافة على طول المحور z عند z=4، وتربط الحواف المتبقية نقاط النهاية هذه. لنفترض الآن أنك تريد حساب الزاوية بين الحافتين الخضراويتين، وقد يكون ذلك مملًا إلى حدٍ ما باستخدام علم المثلثات، لذا يمكنك: تشكيل شعاع لكل حافة خضراء (من خلال طرح نقاط النهاية). توحيد كل شعاع منهما. استخدام قاعدة الجداء النقطي لحساب الزاوية بينهما. إذًا لنطبّق هذه الخطوات لحساب الزاوية بين الحافتين الخضراويتين كما يلي: أولًا، نقاط النهاية هي: (0‎, 3, 0) إلى (2‎, 0, 0) (0‎, 3, 0) إلى (0‎, 0, 4) ثانيًا، نحسب أشعة الإزاحة وهي: a = (2, 0, 0) - (0, 3, 0) = (2, -3, 0)T‎ b = (0, 0, 4) - (0, 3, 0) = (0, -3, 4)T‎ ثالثًا، نحسب الأطوال كما يلي: ‎| a |2 = (2, -3, 0)T · (2, -3, 0)T = 13 ‎| b |2 = (0, -3, 4)T · (0, -3, 4)T = 25 رابعًا، نوحّد الأشعة كما يلي: au = (2, -3, 0)T / √13‎ bu = (0, -3, 4)T / 5‎ خامسًا، نحسب الجداء النقطي لهما: au · bu = (2, -3, 0)T · (0, -3, 4)T / (5 √13) = 9 / (5 √13) = 0.49923‎ سادسًا، نحسب الزاوية بينهما كما يلي: cosθ = 0.49923‎ θ = arc cos 0.49923 = 60.051‎ درجة إذا حسبتَ أشعة الإزاحة بالطريقة المعاكسة (بالطرح من ‎(0, 3, 0)‎)، فلن يؤثر ذلك على الإجابة، إذ يمكن أن يؤشّر شعاعان إلى اتجاهين متعاكسين، ولكن حاصل جدائهما النقطي سيكون نفسه. بهذا نكون قد وصلنا إلى نهاية هذا المقال الذي تعرّفنا فيه على كيفية حساب الزاوية بين شعاعين في فضاء ثلاثي الأبعاد، وسنتعرّف في المقال التالي على إسقاط شعاعٍ ما على شعاع آخر. ترجمة -وبتصرُّف- للفصل The Angle between 3D Vectors من كتاب Vector Math for 3D Computer Graphics لصاحبه Bradley Kjell. اقرأ أيضًا المقال السابق: كيفية إيجاد الزاوية بين شعاعين في فضاء ثنائي الأبعاد 2D الجداء النقطي وارتباطه بطول الأشعة وتعامدها في التصاميم 3D تطبيق الجداء النقطي Dot Product على الأشعة في التصاميم 3D تغيير حجم شعاع Scaling في التصاميم ثلاثية الأبعاد 3D
  25. من المفيد معرفة وحدات بايثون Python المختلفة لتحرير جداول البيانات وتنزيل الملفات وتشغيل البرامج، ولكن لا توجد في بعض الأحيان أيّ وحداتٍ للتطبيقات التي تحتاج إلى العمل معها، فالأدوات الأساسية لأتمتة المهام على حاسوبك هي البرامج التي تكتبها وتتحكم في لوحة المفاتيح والفأرة مباشرةً، حيث يمكن لهذه البرامج التحكم في التطبيقات الأخرى من خلال إرسال ضغطات مفاتيح افتراضية ونقرات افتراضية بالفأرة إليها كما لو أنك جالس أمام حاسوبك وتتفاعل مع التطبيقات بنفسك. تُعرَف هذه التقنية باسم أتمتة واجهة المستخدم الرسومية Graphical User Interface Automation أو GUI automation اختصارًا، حيث يمكن لبرامجك باستخدام هذه التقنية فعل أيّ شيء يمكن أن يفعله المستخدم الجالس أمام الحاسوب باستثناء سكب القهوة على لوحة المفاتيح طبعًا. يمكن عَدّ أتمتة واجهة المستخدم الرسومية كبرمجة ذراع آلية، حيث يمكنك برمجة الذراع الآلية للكتابة باستخدام لوحة المفاتيح وتحريك الفأرة نيابةً عنك، وتُعَد هذه التقنية مفيدة خاصةً للمهام التي تتضمن الكثير من النقر أو ملء الاستمارات. تبيع بعض الشركات حلولَ الأتمتة المبتكرة والمكلفة، والتي تُسوَّق عادةً بأنها أتمتة العمليات الآلية Robotic Process Automation أو RPA اختصارًا، حيث لا تختلف هذه المنتجات فعليًا عن سكربتات بايثون التي يمكنك إنشاؤها بنفسك باستخدام الوحدة pyautogui التي تحتوي على دوال لمحاكاة حركات الفأرة ونقرات الأزرار وتمرير عجلة الفأرة. سنوضّح في هذا المقال مجموعة فرعية فقط من ميزات الوحدة PyAutoGUI، حيث يمكنك العثور على التوثيق الكامل على موقعها الرسمي. تثبيت الوحدة pyautogui يمكن لوحدة pyautogui إرسال ضغطات المفاتيح ونقرات الفأرة الافتراضية إلى أنظمة تشغيل ويندوز Windows وماك macOS ولينكس Linux، حيث يمكن لمستخدمي ويندوز وماك macOS ببساطة استخدام أداة pip لتثبيت الوحدة PyAutoGUI، ولكن يجب على مستخدمي نظام لينكس أولًا تثبيت بعض البرامج التي تعتمد عليها وحدة PyAutoGUI، لذا افتح نافذة طرفية Terminal وأدخِل الأوامر التالية: sudo apt install scrot python3-tk python3-dev يمكنك تثبيت الوحدة PyAutoGUI من خلال تشغيل الأمر pip install --user pyautogui، ولكن لا تستخدم الأمر sudo مع الأداة pip، إذ يمكن أن تثبِّتَ وحداتٍ مع تثبيت بايثون الذي يستخدمه نظام التشغيل، مما يتسبب في حدوث تعارضات مع أيّ سكربتات تعتمد على ضبطها الأصلي، ولكن يجب استخدام الأمر sudo عند تثبيت التطبيقات باستخدام apt. يمكنك اختبار صحة تثبيت الوحدة PyAutoGUI من خلال تشغيل الأمر import pyautogui في الصدفة التفاعلية Interactive Shell والتحقق من وجود رسائل خطأ. ملاحظة: لا تحفظ برنامجك بالاسم pyautogui.py، إذ ستستورد لغة بايثون برنامجك بدلًا من الوحدة PyAutoGUI وستتلقّى رسائل خطأ مثل الرسالة AttributeError: module 'pyautogui' has no attribute 'click'‎ عند تشغيل الأمر import pyautogui. إعداد تطبيقات إمكانية الوصول Accessibility على نظام ماك macOS لا يسمح نظام ماك للبرامج بالتحكم في الفأرة أو لوحة المفاتيح كإجراءٍ أمني، لذا يجب ضبط البرنامج الذي يشغّل سكربت بايثون ليكون تطبيقًا لإمكانية الوصول لكي تعمل وحدة PyAutoGUI على نظام تشغيل ماك، إذ لن يكون لاستدعاءات دوال PyAutoGUI أيّ تأثير بدون إجراء هذه الخطوة. اجعل تطبيقك مفتوحًا سواء شغّلته من محرّر Mu أو بيئة IDLE أو الطرفية Terminal، ثم افتح "تفضيلات النظام System Preferences" وانتقل إلى التبويب "إمكانية الوصول Accessibility". ستظهر التطبيقات المفتوحة حاليًا تحت العنوان "السماح للتطبيقات التالية بالتحكم في حاسوبك Allow the apps below to control your computer". تحقّق من تطبيق Mu أو IDLE أو الطرفية Terminal أو أيّ تطبيق تستخدمه لتشغيل سكربتات بايثون الخاصة بك، وسيُطلَب منك إدخال كلمة مرورك لتأكيد هذه التغييرات. البقاء على المسار الصحيح يجب أن تعرف كيفية التهرب من المشكلات التي قد تواجهك قبل الانتقال إلى أتمتة واجهة المستخدم الرسومية، فمثلًا يمكن لسكربت بايثون تحريك الفأرة والكتابة من خلال ضغطات المفاتيح بسرعة مذهلة، وقد يكون الأمر سريعًا جدًا بحيث لا تتمكّن البرامج الأخرى من مجاراة هذه السرعة، وإذا حدث خطأٌ ما مع استمرار برنامجك في تحريك الفأرة، فسيكون من الصعب معرفة ما يفعله البرنامج بالضبط أو كيفية حل هذه المشكلة. كما يمكن أن يخرج برنامجك عن السيطرة بالرغم من أنه يتبع تعليماتك بطريقة مثالية مثل المكانس المسحورة من فيلم The Sorcerer’s Apprentice من إنتاج شركة ديزني، والتي ظلت تملأ حوض ميكي بالماء ثم تملأه أكثر من اللازم، وقد يكون إيقاف البرنامج أمرًا صعبًا إذا كانت الفأرة تتحرك من تلقاء نفسها، مما يمنعك من النقر على نافذة محرّر Mu لإغلاقه. توجد لحسن الحظ عدة طرق لمنع مشاكل أتمتة واجهة المستخدم الرسومية أو حلها، والتي سنوضّحها فيما يلي. التوقف المؤقت والفشل الآمن إذا ظهر خطأ في برنامجك ولم تتمكّن من استخدام لوحة المفاتيح والفأرة لإغلاقه، فيمكنك استخدام ميزة الفشل الآمن في وحدة PyAutoGUI. حرّك الفأرة بسرعة إلى إحدى زوايا الشاشة الأربعة مثلًا، حيث يكون لكل استدعاء للدالة الخاصة بوحدة PyAutoGUI تأخير قدره 10 جزء من الثانية بعد تنفيذ الإجراء الخاص بها ليمنحك وقتًا كافيًا لتحريك الفأرة إلى الزاوية. إذا وجدَت وحدة PyAutoGUI بعد ذلك أن مؤشر الفأرة في الزاوية، فستطلق الاستثناء pyautogui.FailSafeException. لن يكون للتعليمات التي ليست تابعة لوحدة PyAutoGUI هذا التأخير الذي مقداره 10 جزء من الثانية. إذا وجدت نفسك في موقف تحتاج فيه إلى إيقاف برنامج PyAutoGUI، فما عليك سوى تحريك الفأرة بسرعة باتجاه الزاوية لإيقافه. إغلاق كل شيء من خلال تسجيل الخروج قد تكون أبسط طريقة لإيقاف برنامج أتمتة واجهة المستخدم الرسومية الخارج عن السيطرة هي تسجيل الخروج، مما يؤدي إلى إيقاف تشغيل جميع البرامج التي تكون قيد التشغيل. مفتاح اختصار تسجيل الخروج هو CTRL-ALT-DEL في نظامي ويندوز ولينكس، وهو ‎‎-SHIFT-OPTION-Q على نظام ماك. ستفقد أيّ عمل غير محفوظ عند تسجيل الخروج، ولكنك لن تضطر إلى الانتظار حتى تنتهي عملية إعادة التشغيل الكاملة للحاسوب. التحكم في حركة الفأرة ستتعلّم في هذا القسم كيفية تحريك الفأرة وتعقّب موضعها على الشاشة باستخدام الوحدة PyAutoGUI، ولكن يجب أولًا أن تفهم كيفية عمل هذه الوحدة مع الإحداثيات. تستخدم دوال الفأرة الخاصة بوحدة PyAutoGUI إحداثيات x و y، حيث يبين الشكل التالي نظام إحداثيات شاشة الحاسوب، وهو مشابه لنظام الإحداثيات المستخدَم مع الصور الذي ناقشناه في المقال السابق، إذ توجد نقطة الأصل Origin حيث تكون قيمة x و y صفر في الزاوية العلوية اليسرى من الشاشة، وتزداد إحداثيات x باتجاه اليمين، وتزداد إحداثيات y باتجاه الأسفل. تكون جميع الإحداثيات أعدادًا صحيحة موجبة، إذ لا توجد إحداثيات سالبة. إحداثيات شاشة الحاسوب بدقة مقدارها 1920‎×1080 تمثّل الدقة Resolution عدد البكسلات لعرض وطول الشاشة، حيث إذا كانت دقة شاشتك مضبوطة على القيمة ‎1920×1080، فستكون إحداثيات الزاوية العلوية اليسرى هو ‎(0, 0)‎، وستكون إحداثيات الزاوية السفلية اليمنى هو ‎(1919, 1079)‎. تعيد الدالة pyautogui.size()‎ مجموعةً Tuple مكوّنة من عددين صحيحين لعرض الشاشة وارتفاعها بالبكسل. لندخِل مثلًا ما يلي في الصدفة التفاعلية: >>> import pyautogui >>> wh = pyautogui.size() # الحصول على دقة الشاشة >>> wh Size(width=1920, height=1080) >>> wh[0] 1920 >>> wh.width 1920 تعيد الدالة pyautogui.size()‎ المجموعة ‎(1920, 1080)‎‎ على حاسوب دقته 1920‎×1080، إذ قد تختلف القيمة المُعادة اعتمادًا على دقة شاشتك. يُعَد الكائن Size الذي تعيده الدالة size()‎ مجموعة مُسمَّاة Named Tuples، حيث يكون للمجموعات المُسماة فهارس رقمية مثل المجموعات العادية وأسماء سمات Attribute مثل الكائنات، إذ يُقيَّم كل من wh[0]‎ و wh.width بأنه عرض الشاشة. لن نشرح المجموعات المُسمَّاة في هذا المقال، ولكن تذكّر فقط أنه يمكنك استخدامها باستخدام الطريقة نفسها التي تستخدم بها المجموعات العادية. تحريك الفأرة تعرّفنا على مفهوم إحداثيات الشاشة، ويمكننا الآن تحريك الفأرة، حيث تحرّك الدالة pyautogui.moveTo()‎ مؤشر الفأرة مباشرةً إلى موضعٍ محدّد على الشاشة. تشكّل القيم الصحيحة لإحداثيات x و y الوسيطين الأول والثاني لهذه الدالة على التوالي، ويحدّد وسيط الكلمات المفتاحية Keyword Argument الاختياري duration -الذي هو عدد صحيح أو عشري- عدد الثواني التي يجب أن يستغرقها تحريك الفأرة للوصول إلى وِجهتها، وإذا تركتَ هذا الوسيط دون تحديد، فإن القيمة الافتراضية هي 0 للحركة الفورية، وتكون جميع وسطاء الكلمات المفتاحية duration في دوال PyAutoGUI اختيارية. أدخِل مثلًا ما يلي في الصدفة التفاعلية: >>> import pyautogui >>> for i in range(10): # تحريك الفأرة في مربع ... pyautogui.moveTo(100, 100, duration=0.25) ... pyautogui.moveTo(200, 100, duration=0.25) ... pyautogui.moveTo(200, 200, duration=0.25) ... pyautogui.moveTo(100, 200, duration=0.25) يحرّك المثال السابق مؤشر الفأرة باتجاه عقارب الساعة وفق نمطٍ مربع بين الإحداثيات الأربعة المُعطات 10 مرات، حيث تستغرق كل حركة ربع ثانية كما يحدّده وسيط الكلمات المفتاحية duration=0.25، وإن لم تمرّر الوسيط الثالث إلى أيٍّ من استدعاءات الدالة pyautogui.moveTo()‎، فسينتقل مؤشر الفأرة من نقطة إلى أخرى مباشرةً. تحرّك الدالة pyautogui.move()‎ مؤشر الفأرة نسبةً إلى موضعه الحالي، حيث يحرّك المثال التالي الفأرة وفق نمط المربع نفسه، ولكنه يبدأ المربع من أيّ مكان توجد فيه الفأرة على الشاشة عند بدء تشغيل الشيفرة البرمجية: >>> import pyautogui >>> for i in range(10): ... pyautogui.move(100, 0, duration=0.25) # إلى اليمين ... pyautogui.move(0, 100, duration=0.25) # للأسفل ... pyautogui.move(-100, 0, duration=0.25) # إلى اليسار ... pyautogui.move(0, -100, duration=0.25) # للأعلى تأخذ الدالة pyautogui.move()‎ أيضًا ثلاثة وسطاء هي: عدد البكسلات التي يجب تحريكها أفقيًا إلى اليمين، وعدد البكسلات التي يجب تحريكها عموديًا للأسفل، والمدة التي يجب أن يستغرقها إكمال الحركة (اختياريًا). سيؤدي استخدام العدد الصحيح السالب مع الوسيط الأول أو الثاني إلى تحريك الفأرة إلى اليسار أو للأعلى على التوالي. الحصول على موضع الفأرة يمكنك تحديد موضع الفأرة الحالي من خلال استدعاء الدالة pyautogui.position()‎ التي ستعيد المجموعة المُسمَّاة Point لموضعي x و y الخاصين بمؤشر الفأرة عند استدعاء الدالة. أدخِل مثلًا ما يلي في الصدفة التفاعلية مع تحريك الفأرة بعد كل استدعاء: >>> pyautogui.position() # الحصول على موضع الفأرة الحالي Point(x=311, y=622) >>> pyautogui.position() # الحصول على موضع الفأرة الحالي مرة أخرى Point(x=377, y=481) >>> p = pyautogui.position() # الحصول على موضع الفأرة الحالي مرة أخرى >>> p Point(x=1536, y=637) >>> p[0] # ‫يقع الإحداثي x عند الفهرس 0 1536 >>> p.x # يوجد الإحداثي‫ x أيضًا في السمة x 1536 ستختلف قيمك المُعادة اعتمادًا على مكان مؤشر الفأرة. التحكم في تفاعل الفأرة تعرّفتَ كيفية تحريك الفأرة ومعرفة مكانها على الشاشة، وأصبحتَ الآن جاهزًا لبدء النقر والسحب والتمرير. النقر باستخدام الفأرة يمكنك إرسال نقرة افتراضية باستخدام الفأرة إلى حاسوبك من خلال استدعاء التابع pyautogui.click()‎، حيث تستخدم هذه النقرة زر الفأرة الأيسر افتراضيًا وتُطبَّق في أيّ مكان يوجد فيه مؤشر الفأرة حاليًا. يمكنك تمرير إحداثيات x و y لهذه النقرة كوسيط أول وثانٍ اختياريين إلى التابع إذا أدرتَ أن تُطبَّق في مكانٍ آخر غير موضع الفأرة الحالي. إذا أدرتَ تحديد زر الفأرة الذي يجب استخدامه، فضمّن وسيط الكلمات المفتاحية button مع قيم 'left' أو 'middle' أو 'right'، فمثلًا سيؤدي الاستدعاء pyautogui.click(100, 150, button='left')‎ إلى النقر على زر الفأرة الأيسر عند الإحداثيات ‎(100, 150)‎، بينما سيؤدي الاستدعاء pyautogui.click(200, 250, button='right')‎ إلى النقر بزر الفأرة الأيمن عند الإحداثيات ‎(200, 250)‎. أدخِل مثلًا ما يلي في الصدفة التفاعلية: >>> import pyautogui >>> pyautogui.click(10, 5) # ‫تحريك الفأرة إلى الإحداثيات ‎(10, 5)‎ ثم النقر يُفترَض أن ترى مؤشر الفأرة يتحرك بالقرب من الزاوية العلوية اليسرى من الشاشة ثم يحدث النقر مرة واحدة. يمكن تعريف "النقرة" الكاملة على أنها الضغط على زر الفأرة للأسفل ثم تحريره للأعلى دون تحريك المؤشر، ويمكنك أيضًا إجراء نقرة من خلال استدعاء الدالة pyautogui.mouseDown()‎ التي تضغط زر الفأرة للأسفل فقط، ثم استدعاء الدالة pyautogui.mouseUp()‎ التي تحرّر الزر. تمتلك هاتان الدالتان وسطاء الدالة click()‎ نفسها، ولكن تُعَد الدالة click()‎ مجرد دالة مغلِّفة ملائمة لهاتين الدالتين. تنقر الدالة pyautogui.doubleClick()‎ نقرتين باستخدام زر الفأرة الأيسر، بينما تنقر الدالتان pyautogui.rightClick()‎ و pyautogui.middleClick()‎ نقرة واحدة باستخدام زري الفأرة الأيمن والأوسط على التوالي. سحب Dragging الفأرة يعني السحب تحريكَ الفأرة أثناء الضغط باستمرار على أحد أزرارها، فمثلًا يمكنك نقل الملفات بين المجلدات من خلال سحب أيقونات المجلدات، أو يمكنك نقل المواعيد في تطبيق التقويم من خلال سحبها باستخدام الفأرة. توفر وحدة PyAutoGUI الدالتين pyautogui.dragTo()‎ و pyautogui.drag()‎ لسحب مؤشر الفأرة إلى موقع جديد أو موقع متعلق بموقعه الحالي. تستخدم الدالتان dragTo()‎ و drag()‎ وسطاء الدالتين moveTo()‎ و move()‎ نفسها وهي: حركة الإحداثي x أو الحركة أفقيًا وحركة الإحداثي y أو الحركة عموديًا ومدة زمنية اختيارية. لا يطبّق نظام ماك السحب تطبيقًا صحيحًا عندما تتحرك الفأرة بسرعة كبيرة، لذا يوصَى بتمرير وسيط الكلمات المفتاحية duration. لنجرّب هذه الدوال، لذا افتح تطبيق رسمٍ مثل تطبيق الرسام MS Paint على ويندوز أو تطبيق Paintbrush على نظام ماك ، أو تطبيق GNU Paint على نظام لينكس، حيث سنستخدم وحدة PyAutoGUI للرسم في هذه التطبيقات. إن لم يكن لديك تطبيق رسم، فيمكنك استخدام تطبيق sumopaint عبر الإنترنت. أدخِل ما يلي في نافذة ملفٍ جديد في محرّرك واحفظه بالاسم spiralDraw.py مع وجود مؤشر الفأرة على لوحة الرسم الخاصة بتطبيق الرسم وتحديد أداة القلم Pencil أو الفرشاة Brush: import pyautogui, time ➊ time.sleep(5) ➋ pyautogui.click() # النقر لتنشيط النافذة distance = 300 change = 20 while distance > 0: ➌ pyautogui.drag(distance, 0, duration=0.2) # التحرك يمينًا ➍ distance = distance – change ➎ pyautogui.drag(0, distance, duration=0.2) # التحرك للأسفل ➏ pyautogui.drag(-distance, 0, duration=0.2) # التحرك يسارًا distance = distance – change pyautogui.drag(0, -distance, duration=0.2) # التحرك للأعلى سيكون هناك تأخير لمدة خمس ثوانٍ ➊ عند تشغيل هذا البرنامج لتتمكّن من تحريك مؤشر الفأرة على نافذة برنامج الرسم مع تحديد أداة القلم أو الفرشاة، ثم سيتحكّم برنامج spiralDraw.py في الفأرة وينقر لتنشيط نافذة برنامج الرسم ➋. النافذة النشطة هي النافذة التي تقبل حاليًا الإدخال من لوحة المفاتيح، وستؤثّر الإجراءات التي تتخذها مثل الكتابة أو سحب الفأرة على تلك النافذة، وتُعرَف النافذة النشطة أيضًا بالنافذة المُركَّزة أو النافذة الأمامية. يرسم برنامج spiralDraw.py نمطًا حلزونيًا مربعًا مثل النمط الموجود على يسار الشكل الآتي بعد أن يصبح برنامج الرسم نشطًا. يمكنك أيضًا إنشاء صورة حلزونية مربعة باستخدام الوحدة Pillow التي ناقشناها في المقال السابق، ولكن يتيح لك إنشاء الصورة من خلال التحكم في الفأرة لرسمها في برنامج الرسام MS Paint الاستفادةَ من أنماط الفرشاة المتنوعة لهذا البرنامج كما في الشكل الموجود على يمين الشكل التالي، بالإضافة إلى ميزات متقدمة أخرى مثل التدرجات أو أداة التعبئة، حيث يمكنك تحديد إعدادات الفرشاة مسبقًا بنفسك أو جعل شيفرة بايثون الخاصة بك تحدّد هذه الإعدادات، ثم يمكنك تشغيل برنامج الرسم الحلزوني. نتائج مثال استخدام الدالة pyautogui.drag()‎ المرسومة باستخدام فُرش برنامج الرسام المختلفة يبدأ المتغير distance عند القيمة 200، لذلك يسحب استدعاء الدالة drag()‎ الأول المؤشر بمقدار 200 بكسل إلى اليمين، ويستغرق ذلك 0.2 ثانية ➌ في التكرار الأول لحلقة while، ثم تُقلَّل قيمة المتغير distance إلى القيمة 195 ➍، ويسحب استدعاء الدالة drag()‎ الثاني المؤشر بمقدار 195 بكسل للأسفل ➎. يسحب استدعاء الدالة drag()‎ الثالث المؤشر بمقدار ‎-195 أفقيًا (أي بمقدار 195 إلى اليسار) ➏، وتُقلَّل قيمة المتغير distance إلى 190، ويسحب استدعاء drag()‎ الأخير المؤشر بمقدار 190 بكسل للأعلى. تُسحَب الفأرة إلى اليمين والأسفل واليسار والأعلى في كل تكرار، وتكون قيمة المتغير distance أصغر قليلًا مما كانت عليه في التكرار السابق. يمكنك تحريك مؤشر الفأرة لرسم شكل حلزوني مربع من خلال تكرار هذه الشيفرة البرمجية. يمكنك رسم هذا الحلزوني يدويًا (أو باستخدام الفأرة)، ولكن يجب أن تعمل ببطء لتكون دقيقًا جدًا، ولكن يمكن للوحدة PyAutoGUI إنجاز ذلك في بضع ثوانٍ. ملاحظة: لا تستطيع الوحدة PyAutoGUI حاليًا إرسال نقرات الفأرة أو ضغطات المفاتيح إلى برامج معينة مثل برامج مكافحة الفيروسات (لمنع الفيروسات من تعطيل البرنامج) أو ألعاب الفيديو على نظام ويندوز (التي تستخدم طريقة مختلفة لتلقي دخل الفأرة ولوحة المفاتيح). يمكنك التحقق من توثيق الوحدة PyAutoGUI على موقعها الرسمي لمعرفة ما إذا كانت هذه الميزات مدعومة في نظامك. التمرير بالفأرة دالة الوحدة PyAutoGUI الأخيرة الخاصة بالفأرة هي الدالة scroll()‎ التي نمرّر إليها وسيطًا نوعه عدد صحيح يمثّل عدد الوحدات التي تريد تمرير الفأرة عبرها للأعلى أو للأسفل، حيث يختلف حجم هذه الوحدة باختلاف نظام التشغيل والتطبيق، لذا يجب اأن تجرّب لمعرفة مقدار التمرير في حالتك، ويُطبَّق التمرير عند الموضع الحالي لمؤشر الفأرة. يؤدي تمرير عدد صحيح موجب إلى التمرير للأعلى، بينما يؤدي تمرير عدد صحيح سالب إلى التمرير للأسفل. شغّل ما يلي في الصدفة التفاعلية للمحرّر Mu أثناء وجود مؤشر الفأرة على نافذة هذا المحرّر: >>> pyautogui.scroll(200) سترى أن برنامج Mu يُمرَّر للأعلى إذا كان مؤشر الفأرة على حقل نصي يمكن تمريره للأعلى. تخطيط حركات الفأرة إحدى صعوبات كتابة برنامج يؤتمت عملية النقر على الشاشة هي العثور على إحداثيات x و y للأشياء التي ترغب في النقر عليها، ولكن يمكن أن تساعدك الدالة pyautogui.mouseInfo()‎ في هذا الأمر، حيث يُفترَض استدعاء هذه الدالة من الصدفة التفاعلية، وليس كجزء من برنامجك. تشغِّل هذه الدالة تطبيقًا صغيرًا اسمه MouseInfo المُضمَّن مع الوحدة PyAutoGUI، وتبدو نافذة هذا التطبيق كما يلي: نافذة التطبيق MouseInfo أدخِل الآن ما يلي في الصدفة التفاعلية: >>> import pyautogui >>> pyautogui.mouseInfo() يؤدي ذلك إلى ظهور نافذة تطبيق MouseInfo، حيث توفر لك هذه النافذة معلومات حول الموضع الحالي لمؤشر الفأرة، بالإضافة إلى لون البكسل الموجود تحت مؤشر الفأرة كمجموعة RGB مكونة من ثلاثة أعداد صحيحة وكقيمة ست عشرية، حيث يظهر اللون نفسه في مربع اللون Color الموجود في النافذة. يمكنك تسجيل معلومات الإحداثيات أو البكسلات من خلال النقر على أحد أزرار النسخ Copy أو التسجيل Log الثمانية، حيث ستنسخ أزرار Copy All و Copy XY و Copy RGB و Copy RGB Hex معلوماتها الخاصة في الحافظة Clipboard، وستكتب الأزرار Log All و Log XY و Log RGB و Log RGB Hex معلوماتها الخاصة في الحقل النصي الكبير من هذه النافذة، ويمكنك حفظ النص الموجود في هذا الحقل لتسجيل النص بالنقر على زر الحفظ Save Log. لاحظ تحديد مربع الاختيار ‎3 Sec. Button Delay‎ افتراضيًا، مما يتسبب في تأخير لمدة 3 ثوانٍ بين النقر على زر النسخ Copy أو التسجيل Log وحدوث النسخ أو التسجيل، ويمنحك ذلك وقتًا قصيرًا للنقر على الزر ثم تحريك الفأرة إلى الموضع المطلوب. قد يكون من الأسهل إلغاء تحديد مربع الاختيار 3‎ Sec. Button Delay، وتحريك الفأرة إلى موضعٍ معين، ثم الضغط على المفاتيح من F1 إلى F8 لنسخ موضع الفأرة أو تسجيله. يمكنك إلقاء نظرة على قوائم النسخ والتسجيل الموجودة أعلى نافذة تطبيق MouseInfo لمعرفة المفاتيح المرتبطة بهذه الأزرار. ألغِ مثلًا تحديد مربع الاختيار ‎3 Sec. Button Delay‎، ثم حرّك الفأرة على الشاشة أثناء الضغط على الزر F6، ولاحظ كيفية تسجيل إحداثيات x و y للفأرة في الحقل النصي الكبير في منتصف النافذة، حيث يمكنك لاحقًا استخدام هذه الإحداثيات في سكربتات PyAutoGUI الخاصة بك. اطّلع على توثيق تطبيق MouseInfo الكامل لمزيدٍ من المعلومات. العمل مع الشاشات ليس من الضروري أن تنقر أو تكتب برامجُك لأتمتة واجهة المستخدم الرسومية دون رؤية ما يحدث، إذ تحتوي الوحدة PyAutoGUI على ميزات لقطة الشاشة التي يمكنها إنشاء ملف صورة بناءً على محتويات الشاشة الحالية، ويمكن لهذه الدوال أيضًا إعادة كائن Image الخاص بالوحدة Pillow لمظهر الشاشة الحالي. اطّلع على المقال السابق وثبّت الوحدة pillow قبل الاستمرار في هذا القسم. يجب أن تثبّت برنامج scrot على الحواسيب التي تعمل على نظام لينكس لاستخدام دوال لقطة الشاشة في الوحدة PyAutoGUI، لذا شغّل الأمر sudo apt install scrot في نافذةالطرفية لتثبيت هذا البرنامج. إذا كنت تستخدم نظام تشغيل ويندوز أو ماك، فانتقل إلى الخطوة التالية من هذا القسم. الحصول على لقطة الشاشة يمكنك التقاط لقطات شاشة في لغة بايثون من خلال استدعاء الدالة pyautogui.screenshot()‎، لذا أدخِل ما يلي في الصدفة التفاعلية: >>> import pyautogui >>> im = pyautogui.screenshot() سيحتوي المتغير im على كائن Image للقطة الشاشة، وبالتالي يمكنك الآن استدعاء التوابع مع كائن Image في المتغير im مثل أيّ كائن Image آخر. اطّلع على المقال السابق للحصول على مزيدٍ من التفاصيل حول كائنات Image. تحليل لقطة الشاشة لنفترض أن إحدى الخطوات في برنامجك لأتمتة واجهة المستخدم الرسومية هي النقر على زر رمادي، حيث يمكنك التقاط لقطة شاشة قبل استدعاء التابع click()‎ وإلقاء نظرة على البكسل الذي سينقر سكربتك عليه، فإن لم يكن لون هذا البكسل رماديًا مثل الزر الرمادي، فهذا يعني أن برنامجك يعلم أن هناك خطأً ما، وبالتالي قد تتحرك النافذة بطريقة غير متوقعة، أو قد يوقِف مربع حوار منبثق الزر. يمكن لبرنامجك عندها رؤية أنه لا ينقر على الشيء الصحيح ويوقِف نفسه بدلًا من الاستمرار وإحداث فوضى من خلال النقر على الشيء الخطأ. يمكنك الحصول على قيمة لون RGB لبكسل معين على الشاشة باستخدام الدالة pixel()‎. أدخِل مثلًا ما يلي في الصدفة التفاعلية: >>> import pyautogui >>> pyautogui.pixel((0, 0)) (176, 176, 175) >>> pyautogui.pixel((50, 200)) (130, 135, 144) مرّر مجموعة من الإحداثيات مثل ‎(0, 0)‎ أو ‎(50, 200)‎ إلى الدالة pixel()‎ التي ستخبرك بلون البكسل عند تلك الإحداثيات في صورتك. القيمة المُعادة من الدالة pixel()‎ هي مجموعة RGB مكونة من ثلاثة أعداد صحيحة تمثّل مقدار اللون الأحمر والأخضر والأزرق في البكسل، ولا توجد قيمة رابعة تمثّل قيمة ألفا Alpha، لأن صور لقطة الشاشة معتمة Opaque تمامًا. تعيد الدالة pixelMatchesColor()‎ الخاصة بوحدة PyAutoGUI القيمة True إذا تطابق البكسل الموجود عند إحداثيات x و y المُعطاة على الشاشة مع اللون المُعطَى. يُعَد الوسيطان الأول والثاني أعدادًا صحيحة تمثّل إحداثيات x و y، والوسيط الثالث هو مجموعة مكونة من ثلاثة أعداد صحيحة تمثّل لون RGB الذي يجب أن يتطابق مع البكسل الموجود على الشاشة. أدخِل مثلًا ما يلي في الصدفة التفاعلية: >>> import pyautogui ➊ >>> pyautogui.pixel((50, 200)) (130, 135, 144) ➋ >>> pyautogui.pixelMatchesColor(50, 200, (130, 135, 144)) True ➌ >>> pyautogui.pixelMatchesColor(50, 200, (255, 135, 144)) False استخدمنا الدالة pixel()‎ للحصول على مجموعة RGB التي تمثّل لون البكسل عند إحداثيات مُحدَّدة ➊، وسنمرّر الآن الإحداثيات نفسها ومجموعة RGB إلى الدالة pixelMatchesColor()‎ ➋، والتي يجب أن تعيد القيمة True. نغيّر بعد ذلك قيمةً من مجموعة RGB ونستدعي الدالة pixelMatchesColor()‎ مرةً أخرى مع الإحداثيات نفسها ➌، حيث يجب أن تعيد القيمة False. يمكن أن يكون استدعاء هذه الدالة مفيدًا عندما تكون برامجك لأتمتة واجهة المستخدم الرسومية على وشك استدعاء التابع click()‎ . لاحظ أن اللون عند الإحداثيات المحددة يجب أن يكون متطابقًا تمامًا، حيث إذا كان مختلفًا قليلًا مثل ‎(255, 255, 254)‎ بدلًا من ‎(255, 255, 255)‎، فستعيد الدالة ‎pixelMatchesColor()‎ القيمة False. التعرف على الصور إن لم تكن على معرفة مسبقة بالمكان الذي يجب أن تنقر عليه الوحدة PyAutoGUI، فيمكنك استخدام ميزة التعرّف على الصور، لذا أعطِ وحدة PyAutoGUI صورةً لما تريد النقر عليه ودعه يكتشف الإحداثيات. إذا التقطتَ مسبقًا لقطة شاشة للحصول على صورة زر الإرسال Submit في الملف submit.png مثلًا، فستعيد الدالة locateOnScreen()‎ إحداثيات مكان وجود تلك الصورة. لنتعرّف على كيفية عمل هذه الدالة، لذا حاول التقاط لقطة شاشة لمنطقة صغيرة من شاشتك، ثم احفظ الصورة وأدخِل ما يلي في الصدفة التفاعلية مع وضع اسم ملف لقطة الشاشة الخاصة بك مكان 'submit.png': >>> import pyautogui >>> b = pyautogui.locateOnScreen('submit.png') >>> b Box(left=643, top=745, width=70, height=29) >>> b[0] 643 >>> b.left 643 يُعَد كائن Box مجموعةً مسماة تعيدها الدالة locateOnScreen()‎ وله إحداثي x للحافة اليسرى وإحداثي y للحافة العلوية والعرض والارتفاع لمكان العثور على الصورة الأول على الشاشة. إذا طبّقتَ ذلك على حاسوبك باستخدام لقطة شاشتك، فستكون القيمة المُعادة مختلفة عن القيمة الموضحة في مثالنا. إذا تعذر العثور على الصورة على الشاشة، فستعيد الدالة locateOnScreen()‎ القيمة None. لاحظ أن الصورة الموجودة على الشاشة يجب أن تتطابق تمامًا مع الصورة المُقدَّمة للتعرّف عليها، حيث إذا كانت الصورة مختلفة ببكسل واحد، فسترفع الدالة locateOnScreen()‎ الاستثناء ImageNotFoundException. إذا غيّرتَ دقة الشاشة، فقد لا تتطابق الصور من لقطات الشاشة السابقة مع الصور الموجودة على شاشتك الحالية، لذا يمكنك تغيير القياسات في إعدادات العرض لنظام تشغيلك كما هو موضّح في الشكل التالي: إعدادات قياسات العرض في نظام ويندوز 10 (على اليسار) ونظام ماك (على اليمين) إذا عُثِر على الصورة في عدة أماكن على الشاشة، فستعيد الدالة locateAllOnScreen()‎ كائن Generator الذي يمكنك تمريره إلى التابع list()‎ لإعادة قائمة من المجموعات المكونة من أربعة أعداد صحيحة، حيث ستوجد مجموعة واحدة مكونة من أربعة أعداد صحيحة لكل موقع توجد فيه الصورة على الشاشة. تابع مثال الصدفة التفاعلية من خلال إدخال ما يلي مع وضع ملف الصورة الخاص بك مكان 'submit.png': >>> list(pyautogui.locateAllOnScreen('submit.png')) [(643, 745, 70, 29), (1007, 801, 70, 29)] تمثل كل مجموعة من هذه المجموعات المكونة من أربعة أعداد صحيحة منطقةً من الشاشة، حيث تظهَر الصورة في موقعين في المثال السابق. إذا وُجِدت صورتُك في منطقةٍ واحدة فقط، فسيؤدي استخدام التابع list()‎ والدالة locateAllOnScreen()‎ إلى إعادة قائمة تحتوي على مجموعة واحدة فقط. نحصل على المجموعة المكونة من أربعة أعداد صحيحة التي تمثّل الصورة التي تريد تحديدها، ثم يمكننا النقر على مركز هذه المنطقة من خلال تمرير هذه المجموعة إلى التابع click()‎. لندخل الآن مثلًا ما يلي في الصدفة التفاعلية: >>> pyautogui.click((643, 745, 70, 29)) يمكنك أيضًا تمرير اسم ملف الصورة مباشرةً إلى التابع click()‎ كما يلي: >>> pyautogui.click('submit.png') تقبل الدالتان moveTo()‎ و dragTo()‎ أيضًا وسطاء لاسم ملف الصورة. تذكّر أن الدالة locateOnScreen()‎ ترفع استثناءً إن لم تتمكّن من العثور على الصورة على الشاشة، لذا يجب أن تستدعيها من داخل تعليمة try كما يلي: try: location = pyautogui.locateOnScreen('submit.png') except: print('Image could not be found.') سيؤدي استثناء عدم العثور على الصورة على الشاشة إلى تعطّل برنامجك إن لم تستخدم تعليمات try و except، لذا من الجيد استخدام تعليمات try و except عند استدعاء الدالة locateOnScreen()‎ لأنك لا تستطيع التأكّد من أن برنامجك سيعثر على الصورة دائمًا. الحصول على معلومات النافذة يُعَد التعرّف على الصور طريقة ضعيفة للعثور على الأشياء التي تظهر على الشاشة، حيث إذا كان هناك بكسل واحد بلون مختلف، فلن تتمكن الدالة pyautogui.locateOnScreen()‎ من العثور على الصورة، لذا إذا كنت بحاجة إلى العثور على مكان وجود نافذة معينة على الشاشة، فمن الأسرع والأكثر موثوقية استخدام ميزات النافذة الخاصة بوحدة PyAutoGUI. ملاحظة: تعمل ميزات النافذة الخاصة بوحدة PyAutoGUI على نظام تشغيل ويندوز فقط، ولا تعمل على نظام تشغيل ماك أو لينكس ابتداءً من الإصدار 0.9.46، وتأتي هذه الميزات من احتواء الوحدة PyAutoGUI على الوحدة PyGetWindow. الحصول على النافذة النشطة النافذة النشطة على شاشتك هي النافذة الموجودة حاليًا في المقدمة والتي تقبل الإدخال من لوحة المفاتيح. إذا كنت تكتب حاليًا شيفرة برمجية في المحرّر Mu، فإن نافذته هي النافذة النشطة، حيث ستُنشَّط نافذة واحدة فقط من بين جميع النوافذ التي تظهر على شاشتك في كل مرة. استدعِ الدالة pyautogui.getActiveWindow()‎ في الصدفة التفاعلية للحصول على كائن Window أو كائن Win32Window عند التشغيل على نظام ويندوز. يمكنك استرداد أيٍّ من سمات الكائن Window التي تمثّل حجمه وموضعه وعنوانه بعد الحصول عليه وهذه السمات هي: left و right و top و bottom: عدد صحيح واحد يمثّل الإحداثي x أو y لطرف النافذة. topleft و topright و bottomleft و bottomright: مجموعة مسماة مكونة من عددين صحيحين يمثّلان إحداثيات (x, y) لزاوية النافذة. midleft و midright و midleft و midright: مجموعة مسماة مكونة من عددين صحيحين يمثلان إحداثيات (x, y) لمنتصف طرف النافذة. width و height: عدد صحيح واحد يمثّل أحد أبعاد النافذة بالبكسل. size: مجموعة مسماة مكونة من عددين صحيحين يمثّلان عرض وارتفاع النافذة (width, height). area: عدد صحيح واحد يمثل مساحة النافذة بالبكسل. center: مجموعة مسماة مكونة من عددين صحيحين يمثلان إحداثيات (x, y) لمركز النافذة. centerx و centery : عدد صحيح واحد يمثل إحداثي x أو y لمركز النافذة. box: مجموعة مسماة مكونة من أربعة أعداد صحيحة لقياسات يسار وأعلى وعرض وارتفاع النافذة (left, top, width, height). title: سلسلة من النص الموجود في شريط العنوان أعلى النافذة. أدخِل مثلًا ما يلي في الصدفة التفاعلية للحصول على معلومات موضع النافذة وحجمها وعنوانها من كائن Window: >>> import pyautogui >>> fw = pyautogui.getActiveWindow() >>> fw Win32Window(hWnd=2034368) >>> str(fw) '<Win32Window left="500", top="300", width="2070", height="1208", title="Mu 1.0.1 – test1.py">' >>> fw.title 'Mu 1.0.1 – test1.py' >>> fw.size (2070, 1208) >>> fw.left, fw.top, fw.right, fw.bottom (500, 300, 2070, 1208) >>> fw.topleft (256, 144) >>> fw.area 2500560 >>> pyautogui.click(fw.left + 10, fw.top + 20) يمكنك الآن استخدام هذه السمات لحساب الإحداثيات الدقيقة في النافذة، فمثلًا إذا كنت تعلم أن الزر الذي تريد النقر عليه يقع دائمًا على بعد 10 بكسلات على اليمين و20 بكسلًا للأسفل من الزاوية العلوية اليسرى للنافذة، وأن الزاوية العلوية اليسرى للنافذة تقع عند إحداثيات الشاشة ‎(300, 500)‎، فسيؤدي استدعاء التابع pyautogui.click(310, 520)‎ (أو pyautogui.click(fw.left + 10, fw.top + 20)‎ إذا احتوى المتغير fw على كائن Window الخاص بالنافذة) إلى النقر على هذا الزر، وبالتالي لن تضطر إلى الاعتماد على الدالة locateOnScreen()‎ الأبطأ والأقل موثوقية للعثور على الزر. طرق أخرى للحصول على النافذة تُعَد الدالة getActiveWindow()‎ مفيدةً للحصول على النافذة النشطة في وقت استدعاء الدالة، ولكن ستحتاج إلى استخدام بعض الدوال الأخرى للحصول على كائنات Window للنوافذ الأخرى على الشاشة، حيث تعيد الدوال الأربع التالية قائمةً بكائنات Window، وإن لم تتمكّن من العثور على أيّ نوافذ، فستعيد قائمةً فارغة: pyautogui.getAllWindows()‎: تعيد قائمةً بكائنات Window لكل نافذة مرئية على الشاشة. pyautogui.getWindowsAt(x, y)‎: تعيد قائمةً بكائنات Window لكل نافذة مرئية تتضمن النقطة (x, y). pyautogui.getWindowsWithTitle(title)‎: تعيد قائمةً بكائنات Window لكل نافذة مرئية تتضمن السلسلة النصية title في شريط العنوان الخاص بها. pyautogui.getActiveWindow()‎: تعيد كائن Window للنافذة التي تتلقّى تركيز لوحة المفاتيح حاليًا. تحتوي الوحدة PyAutoGUI أيضًا على الدالة pyautogui.getAllTitles()‎ التي تعيد قائمةً بالسلاسل النصية لكل نافذة مرئية. معالجة النوافذ يمكن لسمات النوافذ أن تفعل أكثر من مجرد إخبارك بحجم النافذة وموضعها، إذ يمكنك أيضًا ضبط قيمها لتغيير حجم النافذة أو تحريكها، فمثلًا أدخِل ما يلي في الصدفة التفاعلية: >>> import pyautogui >>> fw = pyautogui.getActiveWindow() ➊ >>> fw.width # الحصول على العرض الحالي للنافذة 1669 ➋ >>> fw.topleft # الحصول على الموضع الحالي للنافذة (174, 153) ➌ >>> fw.width = 1000 # تغيير حجم العرض ➍ >>> fw.topleft = (800, 400) # تحريك النافذة نستخدم أولًا سمات كائن Window لمعرفة معلومات حول حجم النافذة ➊ وموضعها ➋. يجب أن تتحرك النافذة ➍ وتصبح أضيق ➌ كما في الشكل التالي بعد استدعاء هذه الدوال في المحرّر Mu: نافذة المحرّر Mu قبل (في الأعلى) وبعد (في الأسفل) باستخدام سمات كائن Window لتحريك النافذة وتغيير حجمها يمكنك أيضًا اكتشاف وتغيير حالات تصغير النافذة وتكبيرها وتنشيطها، لذا جرّب إدخال ما يلي في الصدفة التفاعلية: >>> import pyautogui >>> fw = pyautogui.getActiveWindow() ➊ >>> fw.isMaximized # ‫تعيد القيمة True عند تكبير النافذة False ➋ >>> fw.isMinimized # تعيد القيمة‫ True عند تصغير النافذة False ➌ >>> fw.isActive # تعيد القيمة‫ True إذا كانت النافذة نشطة True ➍ >>> fw.maximize() # تكبير النافذة >>> fw.isMaximized True ➎ >>> fw.restore() # التراجع عن إجراء التصغير/التكبير ➏ >>> fw.minimize() # تصغير النافذة >>> import time >>> # ‫الانتظار 5 ثوانٍ أثناء تنشيط نافذة مختلفة: ➐ >>> time.sleep(5); fw.activate() ➑ >>> fw.close() # سيؤدي ذلك إلى إغلاق النافذة التي تكتب فيها تحتوي السمات isMaximized ➊ و isMinimized ➋ و isActive ➌ على قيمٍ منطقية تشير إلى ما إذا كانت النافذة حاليًا في حالة التكبير أو التصغير أو التنشيط أم لا، بينما تغير التوابع maximize()‎ ➍ و minimize()‎ ➏ و activate()‎ ➐ و restore()‎ ➎ حالة النافذة، وسيعيد التابع restore() النافذة إلى حجمها وموضعها السابق بعد تكبير النافذة أو تصغيرها باستخدام التابعين maximize()‎ و minimize()‎. يغلق التابع close()‎ النافذة، ولكن كن حذرًا عند استخدام هذا التابع، لأنه قد يتجاوز أي مربعات حوار للرسائل التي تطلب منك حفظ عملك قبل الخروج من التطبيق. يمكن العثور على التوثيق الكامل لميزة التحكم في النوافذ الخاصة بوحدة PyAutoGUI على موقعها الرسمي. يمكنك أيضًا استخدام هذه الميزات بصورة منفصلة عن الوحدة PyAutoGUI مع الوحدة PyGetWindow الذي يمكنك الاطلاع على توثيقها على موقعها الرسمي. التحكم بلوحة المفاتيح تحتوي الوحدة PyAutoGUI أيضًا على دوال لإرسال ضغطات مفاتيح افتراضية إلى حاسوبك، والتي تمكّنك من ملء الاستمارات أو إدخال نصٍ في التطبيقات. إرسال سلسلة نصية من لوحة المفاتيح ترسل الدالة pyautogui.write()‎ ضغطات مفاتيح افتراضية إلى الحاسوب، حيث يعتمد ما تفعله هذه الضغطات على النافذة النشطة والحقل النصي المُركَّز عليه، لذا قد ترغب أولًا في إرسال نقرة بالفأرة إلى الحقل النصي الذي تريده للتأكد من التركيز عليه. لنستخدم لغة بايثون لكتابة النص "Hello, world!‎" في نافذة محرر الملفات. افتح أولًا نافذة جديدة في محرّر ملفاتك وَضَعها في الزاوية العلوية اليسرى من شاشتك بحيث تنقر وحدة PyAutoGUI في المكان المناسب للتركيز عليها، ثم أدخِل ما يلي في الصدفة التفاعلية: >>> pyautogui.click(100, 200); pyautogui.write('Hello, world!') لاحظ كيف أننا وضعنا أمرين على السطر نفسه، وفصلنا بينهما بفاصلة منقوطة، مما يمنع الصدفة التفاعلية من مطالبتك بالإدخال بين تشغيل هاتين التعليمتين، ويمنعك من التركيز على نافذة جديدة عن طريق الخطأ بين الاستدعائين click()‎ و write()‎ الذي يمكن أن يفسد مثالنا. سترسل شيفرة بايثون أولًا نقرة افتراضية بالفأرة إلى الإحداثيات ‎(100, 200)‎، والتي يجب أن تنقر على نافذة محرّر الملفات لنقل التركيز إليها، وسيرسل استدعاء الدالة write()‎ النص "Hello, world!‎" إلى النافذة، مما يجعلها تبدو كما في الشكل التالي، وبالتالي أصبح لديك الآن شيفرة برمجية يمكن أن تكتب نيابةً عنك. استخدام وحدة PyAutogGUI للنقر على نافذة محرّر الملفات وكتابة النص "Hello, world!‎" فيها ستكتب الدالة ‎write()‎‎ افتراضيًا السلسلة النصية الكاملة مباشرةً، ولكن يمكنك تمرير وسيطٍ ثانٍ اختياري لإضافة توقف قصير بين كل محرف والآخر، وهذا الوسيط هو عدد صحيح أو عشري يمثل عدد الثواني للتوقف مؤقتًا، فمثلًا سينتظر الاستدعاء pyautogui.write('Hello, world!', 0.25)‎ ربع ثانية بعد كتابة الحرف H، وربع ثانية أخرى بعد كتابة الحرف e، وإلخ. قد يكون تأثير الآلة الكاتبة التدريجي مفيدًا للتطبيقات الأبطأ التي لا يمكنها معالجة ضغطات المفاتيح بسرعة كافية للتماشي مع الوحدة PyAutoGUI. ستحاكي أيضًا الوحدة PyAutoGUI الضغط باستمرار على مفتاح SHIFT آليًا بالنسبة للمحارف A أو ! . أسماء المفاتيح لا يُعَد تمثيل كافة المفاتيح باستخدام محارف نصية مفردة أمرًا سهلًا مثل تمثيل المفتاح SHIFT أو مفتاح السهم الأيسر بمحرف واحد، لذا تمثل وحدة PyAutoGUI مفاتيح لوحة المفاتيح هذه بقيم سلاسل نصية قصيرة، فمثلًا نمثّل مفتاح ESC باستخدام السلسلة النصية 'esc' و نمثّل مفتاح ENTER باستخدام السلسلة النصية 'enter'. يمكن تمرير قائمة بالسلاسل النصية لهذه المفاتيح إلى الدالة write()‎ بدلًا من تمرير وسيط سلسلة نصية واحدة، فمثلًا يضغط الاستدعاء التالي على المفتاح A ثم على المفتاح B ثم على مفتاح السهم الأيسر مرتين ويضغط أخيرًا على المفتاحين X و Y: >>> pyautogui.write(['a', 'b', 'left', 'left', 'X', 'Y']) يؤدي الضغط على مفتاح السهم الأيسر إلى تحريك مؤشر لوحة المفاتيح، لذا سينتج عن ذلك الخرج XYab. يوضّح الجدول الآتي السلاسل النصية لمفاتيح لوحة المفاتيح الخاصة بوحدة PyAutoGUI، والتي يمكنك تمريرها إلى الدالة write()‎ لمحاكاة الضغط على أيّ مجموعة من المفاتيح. يمكنك أيضًا الاطلاع على قائمة pyautogui.KEYBOARD_KEYS لرؤية جميع السلاسل النصية لمفاتيح لوحة المفاتيح المحتملة التي ستقبلها وحدة PyAutoGUI. تشير السلسلة النصية 'shift' إلى مفتاح SHIFT الأيسر وهي تكافئ السلسلة النصية 'shiftleft'، وينطبق الأمر نفسه على السلاسل النصية 'ctrl' و 'alt' و 'win' التي تشير جميعها إلى مفتاح الجهة اليسرى من لوحة المفاتيح. يوضح الجدول التالي قيم سمات PyKeyboard: السلسلة النصية لمفتاح لوحة المفاتيح معناها 'a' و 'b' و 'c' و 'A' و 'B' و 'C' و '1' و '2' و '3' و '!' و '@' و '#' وإلخ مفاتيح المحارف المفردة 'enter' (أو 'return' أو '‎\n') مفتاح ENTER 'esc' مفتاح ESC 'shiftleft' و 'shiftright' مفتاحا SHIFT الأيسر والأيمن 'altleft' و 'altright' مفتاحا ALT الأيسر والأيمن 'ctrlleft' و 'ctrlright' مفتاحا CTRL الأيسر والأيمن 'tab' (أو '‎\t') مفتاح TAB 'backspace' و 'delete' مفتاح BACKSPACE ومفتاح DELETE 'pageup' و 'pagedown' مفتاح PAGE UP ومفتاح PAGE DOWN 'home' و 'end' مفتاح HOME ومفتاح END 'up' و 'down' و 'left' و 'right' مفاتيح الأسهم للأعلى وللأسفل وإلى اليسار وإلى اليمين 'f1' و 'f2' و 'f3' وإلخ المفاتيح من F1 إلى F12 'volumemute' و 'volumedown' و 'volumeup' مفاتيح كتم الصوت وخفض مستوى الصوت ورفع مستوى الصوت. لا تحتوي بعض لوحات المفاتيح على هذه المفاتيح، ولكن سيبقى نظام تشغيل حاسوبك قادرًا على فهم محاكاة هذه الضغطات للمفاتيح 'pause' مفتاح PAUSE 'capslock' و 'numlock' و 'scrolllock' مفتاح CAPS LOCK ومفتاح NUM LOCK ومفتاح SCROLL LOCK 'insert' مفتاح INS أو INSERT 'printscreen' مفتاح PRTSC أو PRINT SCREEN 'winleft' و 'winright' مفتاح WIN الأيسر والأيمن على نظام ويندوز 'command' مفتاح Command على نظام ماك ‎ 'option' مفتاح OPTION على نظام ماك الضغط على لوحة المفاتيح وتحريرها ترسل الدالتان pyautogui.keyDown()‎ و pyautogui.keyUp()‎ ضغطات مفاتيح افتراضية وتحريرها إلى الحاسوب مثل الدالتين mouseDown()‎ و mouseUp()‎، ونمرّر إلى هاتين الدالتين سلسلة نصية لمفاتيح لوحة المفاتيح (اطّلع على الجدول السابق) كوسيطٍ لها. توفّر وحدة PyAutoGUI الدالة pyautogui.press()‎ التي تستدعي هاتين الدالتين لمحاكاة ضغطة كاملة على المفاتيح. شغّل الشيفرة البرمجية التالية التي ستكتب محرف إشارة الدولار $ الذي نحصل عليه من خلال الضغط على مفتاح SHIFT مع الضغط على الرقم 4: >>> pyautogui.keyDown('shift'); pyautogui.press('4'); pyautogui.keyUp('shift') تضغط التعليمة السابقة على مفتاح SHIFT، ثم تضغط على (وتحرر) الرقم 4، ثم تحرّر مفتاح SHIFT. إذا أردتَ كتابة سلسلة نصية في حقل نصي، فستكون الدالة write()‎ أكثر ملاءمة، ولكن ستكون الدالة press()‎ الطريقة الأبسط بالنسبة للتطبيقات التي تأخذ أوامرًا ذات مفتاح واحد. مجموعات مفاتيح التشغيل السريع Hotkey أو الاختصارات مفاتيح التشغيل السريع أو الاختصارات هي مجموعة من الضغطات على المفاتيح لاستدعاء بعض وظائف التطبيق، فمفتاح التشغيل السريع الشائع لنسخ تحديدٍ مثلًا هو CTRL-C في نظامي تشغيل ويندوز ولينكس أو ‎ -C في نظام تشغيل ماك. يضغط المستخدم مع الاستمرار على مفتاح CTRL، ثم يضغط على المفتاح C، ثم يحرّر المفتاحين C و CTRL، حيث يمكننا تطبيق ذلك باستخدام الدالتين keyDown()‎ و keyUp()‎ الخاصتين بالوحدة PyAutoGUI من خلال إدخال ما يلي: pyautogui.keyDown('ctrl') pyautogui.keyDown('c') pyautogui.keyUp('c') pyautogui.keyUp('ctrl') يُعَد ذلك معقدًا إلى حدٍ ما، لذا استخدم الدالة pyautogui.hotkey()‎ بدلًا من ذلك، حيث تأخذ هذه الدالة عدة وسطاء تمثّل السلسلة النصية لمفاتيح لوحة المفاتيح، وتضغط عليها بالترتيب، ثم تحررها بالترتيب العكسي، إذ ستكون الشيفرة البرمجية الخاصية بمثال CTRL-C ببساطة كما يلي: pyautogui.hotkey('ctrl', 'c') تُعَد هذه الدالة مفيدة خاصةً لمجموعات مفاتيح التشغيل السريع الأكبر حجمًا، فمثلًا تعرض مجموعة مفاتيح التشغيل السريع Ctrl-Alt-Shift-S لوحة الأنماط Style في برنامج وورد Word، حيث يمكنك استخدام الاستدعاء hotkey('ctrl', 'alt', 'shift', 's')‎ فقط بدلًا من إجراء ثمانية استدعاءات لدوال مختلفة (أربعة استدعاءات للدالة keyDown()‎ وأربعة استدعاءات للدالة keyUp()‎). إعداد سكربتات أتمتة واجهة المستخدم الرسومية تُعَد سكربتات أتمتة واجهة المستخدم الرسومية طريقةً رائعة لأتمتة المهام المملة، ولكنها قد تكون صعبة التحقيق، حيث إذا كان هناك نافذة في مكان خاطئ على سطح المكتب أو ظهرت بعض النوافذ المنبثقة بطريقة غير متوقعة، فقد ينقر السكربت الخاص بك على الأشياء الخاطئة على الشاشة، لذا إليك بعض النصائح لإعداد سكربتات أتمتة واجهة المستخدم الرسومية: استخدم دقة الشاشة نفسها في كل مرة تشغّل فيها السكربت حتى لا يتغير موضع النوافذ. يجب تكبير نافذة التطبيق التي ينقر عليها السكربت الخاص بك بحيث تكون الأزرار والقوائم في المكان نفسه في كل مرة تشغّل فيها السكربت. أضِف فترات توقف كافية أثناء انتظار تحميل المحتوى، إذ لا تريد أن يبدأ السكربت بالنقر قبل أن يصبح التطبيق جاهزًا. استخدم الدالة locateOnScreen()‎ للعثور على الأزرار والقوائم التي يمكنك النقر عليها بدلًا من الاعتماد على إحداثيات XY. إن لم يتمكّن السكربت الخاص بك من العثور على الشيء الذي يريد النقر عليه، فأوقِف البرنامج بدلًا من السماح له بمواصلة النقر عشوائيًا. استخدم الدالة getWindowsWithTitle()‎ للتأكّد من وجود نافذة التطبيق التي تعتقد أن السكربت الخاص بك ينقر عليها، واستخدم التابع activate()‎ لوضع تلك النافذة في المقدمة. استخدم الوحدة logging التي تحدثنا عنها في مقالٍ سابق للاحتفاظ بملفٍ يسجّل ما فعله السكربت الخاص بك، وبالتالي إذا أوقفتَ السكربت في منتصف العملية، فيمكنك تعديله للمتابعة من مكان توقف هذا السكربت. أضِف أكبر عدد ممكن من عمليات التحقق إلى السكربت الخاص بك، فمثلًا يمكن أن يفشل السكربت إذا ظهرت نافذة منبثقة غير متوقعة أو إذا فقدَ حاسوبك اتصاله بالإنترنت. قد ترغب في الإشراف على السكربت عندما يبدأ لأول مرة للتأكد من أنه يعمل بصورة صحيحة. قد ترغب أيضًا في التوقف مؤقتًا في بداية السكربت الخاص بك حتى يتمكّن المستخدم من إعداد النافذة التي سينقر عليها السكربت، حيث تحتوي وحدة PyAutoGUI على الدالة sleep()‎ التي تعمل بطريقة مماثلة للدالة time.sleep()‎، ولكنها توفّر عليك الاضطرار إلى إضافة التعليمة import time إلى السكربتات الخاصة بك، وتوجد أيضًا الدالة countdown()‎ التي تطبع أرقام العد التنازلي لمنح المستخدم إشارة مرئية إلى أن السكربت سيستأنف عمله قريبًا. أدخِل مثلًا ما يلي في الصدفة التفاعلية: >>> import pyautogui >>> pyautogui.sleep(3) # إيقاف البرنامج مؤقتًا لمدة 3 ثوانٍ >>> pyautogui.countdown(10) # العد التنازلي لمدة 10 ثوانٍ 10 9 8 7 6 5 4 3 2 1 >>> print('Starting in ', end=''); pyautogui.countdown(3) Starting in 3 2 1 يمكن أن تساعد هذه النصائح في جعل سكربتات أتمتة واجهة المستخدم الرسومية أسهل في الاستخدام وأكثر قدرة على الاستعادة من الظروف غير المتوقعة. مراجعة لدوال وحدة PyAutoGUI يغطي هذا المقال العديد من الدوال المختلفة، لذا سنوضّح فيما يلي مرجعًا موجزًا سريعًا لهذه الدوال: moveTo(x, y)‎: تحرك مؤشر الفأرة إلى إحداثيات x و y المُحدَّدة. move(xOffset, yOffset)‎: تحرك مؤشر الفأرة بالنسبة إلى موضعه الحالي. dragTo(x, y)‎: تحرك مؤشر الفأرة أثناء الضغط المستمر على زر الفأرة الأيسر. drag(xOffset, yOffset)‎: تحرّك مؤشر الفأرة نسبة إلى موضعه الحالي أثناء الضغط المستمر على زر الفأرة الأيسر. click(x, y, button)‎: تحاكي النقر (بزر الفأرة الأيسر افتراضيًا). rightClick()‎: تحاكي النقر بالزر الأيمن. middleClick()‎: تحاكي النقر بالزر الأوسط. doubleClick()‎: تحاكي النقر المزدوج على الزر الأيسر. mouseDown(x, y, button)‎: تحاكي الضغط على الزر المُحدَّد في الموضع x, y. mouseUp(x, y, button)‎: تحاكي تحرير الزر المُحدَّد في الموضع x, y. scroll(units)‎: تحاكي عجلة التمرير في الفأرة، حيث نمرّر وسيطًا موجبًا للتمرير للأعلى، ونمرّر وسيطًا سالبًا للتمرير للأسفل. write(message)‎: تكتب المحارف الموجودة في سلسلة الرسالة النصية المُحدَّدة. write([key1, key2, key3])‎: تكتب سلاسل نصية لمفاتيح لوحة المفاتيح المُحدَّدة. press(key)‎: تضغط على السلسلة النصية لمفتاحٍ محدَّد من لوحة المفاتيح. keyDown(key)‎: تحاكي الضغط على مفتاح لوحة المفاتيح المُحدَّد. keyUp(key)‎: تحاكي تحرير مفتاح لوحة المفاتيح المُحدَّد. hotkey([key1, key2, key3])‎: تحاكي الضغط على السلاسل النصية لمفاتيح لوحة المفاتيح المُحدَّدة بالترتيب ثم تحرّرها بترتيب عكسي. screenshot()‎: تعيد لقطة الشاشة بوصفها كائن Image. اطّلع على المقال السابق للحصول على معلومات حول كائنات Image. getActiveWindow()‎ و getAllWindows()‎ و getWindowsAt()‎ و getWindowsWithTitle()‎: تعيد هذه الدوال كائنات Window التي يمكنها تغيير حجم نوافذ التطبيقات وإعادة تموضعها على سطح المكتب. getAllTitles()‎: تعيد قائمةً بالسلاسل النصية لشريط عنوان كلّ نافذةٍ على سطح المكتب. اختبارات كابتشا Captcha وأخلاقيات استخدام الحواسيب تُعَد اختبارات كابتشا Captcha اختصارًا للعبارة الإنجليزية "Completely Automated Public Turing test to tell Computers and Humans Apart" أو "اختبار تورينج العام الآلي بالكامل للتمييز بين الحواسيب والبشر"، وهي الاختبارات الصغيرة التي تطلب منك كتابة حروف موجودة في صورة غير واضحة أو النقر على صور صنابير إطفاء الحرائق مثلًا. يسهُل على البشر اجتياز هذه الاختبارات، ولكن يكاد يكون من المستحيل على البرامج حلها بالرغم من أننا نجدها مزعجة. يمكنك أن ترى بعد قراءة هذا المقال مدى سهولة كتابة سكربت يمكنه التسجيل في مليارات حسابات البريد الإلكتروني المجانية مثلًا أو إغراق المستخدمين برسائل مزعجة، لذا تعمل اختبارات كابتشا على تخفيف ذلك من خلال المطالبة بخطوة لا يمكن إلا للبشر اجتيازها. لا تطبق جميع مواقع الويب اختبارات كابتشا، وقد تكون عرضةً لإساءة الاستخدام من المبرمجين غير الأخلاقيين، إذ يُعَد تعلّم البرمجة مهارة قوية ومهمة، ولكن قد تميل إلى إساءة استخدام هذه القوة لتحقيق مكاسب شخصية أو حتى لمجرد التفاخر، فلا يُعَد الباب المفتوح مبررًا للتعدّي على ممتلكات الآخرين، لذا تقع مسؤولية برامجك على عاتقك الشخصي بوصفك مبرمجًا. لا يُعَد التحايل على الأنظمة لإحداث ضررٍ أو انتهاك الخصوصية أو الحصول على ميزة غير عادلة معيارًا للذكاء، لذا نأمل أن تركز على عملك دون الضرر بالآخرين. تطبيق عملي: ملء الاستمارات آليًا يُعَد ملء الاستمارات مهمةً روتينية مملةً جدًا، إذًا لنفترض مثلًا أن لديك كمية هائلة من البيانات في جدول بيانات، ويجب أن تعيد كتابتها في واجهة استمارة تطبيق آخر دون وجود شخص آخر لإنجاز ذلك نيابةً عنك. تحتوي بعض التطبيقات على ميزة الاستيراد التي تسمح لك برفع جدول بيانات يحتوي على المعلومات، ولكن قد لا توجد طريقة أخرى سوى النقر والكتابة دون اهتمام لساعات متواصلة في بعض الأحيان، لذا لنحاول إيجاد وسيلة لأتمتة هذه المهمة المملة. استمارة هذا المشروع هي استمارة على مستندات جوجل Google Docs، والتي يمكنك العثور عليها على autbor.com، وتبدو كما يلي: الاستمارة المستخدمَة في هذا المشروع إليك الخطوات العامة لما يجب أن يفعله برنامجك: النقر على الحقل النصي الأول من الاستمارة. التنقل عبر الاستمارة، وكتابة المعلومات في كل حقل. النقر على زر الإرسال Submit. تكرار العملية مع المجموعة التالية من البيانات. وبالتالي يجب أن تطبّق شيفرتك البرمجية الخطوات التالية: استدعاء الدالة pyautogui.click()‎ للنقر على الاستمارة وزر الإرسال Submit. استدعاء الدالة pyautogui.write()‎ لإدخال النص في الحقول. معالجة الاستثناء KeyboardInterrupt حتى يتمكّن المستخدم من الضغط على الاختصار CTRL-C للإنهاء. افتح نافذة جديدة في محرّرك لإنشاء ملف جديد واحفظه بالاسم formFiller.py. الخطوة الأولى: معرفة الخطوات ملء الاستمارة يجب أولًا معرفة ضغطات المفاتيح ونقرات الفأرة التي ستملأ الاستمارة قبل كتابة الشيفرة البرمجية. يمكن أن يساعدك التطبيق الذي يُشغّله استدعاء الدالة pyautogui.mouseInfo()‎ في معرفة إحداثيات الفأرة المُحدَّدة، ويجب معرفة إحداثيات الحقل النصي الأول فقط، ثم يمكنك الضغط على مفتاح TAB لنقل التركيز إلى الحقل التالي بعد النقر على الحقل الأول، مما يوفّر عليك الاضطرار إلى معرفة إحداثيات x و y للنقر على كل حقل. إليك خطوات إدخال البيانات في الاستمارة: انقل تركيز لوحة المفاتيح على حقل الاسم Name بحيث يؤدي الضغط على المفاتيح إلى كتابة نص في الحقل. اكتب اسمًا، ثم اضغط على مفتاح TAB. اكتب خوفك الأكبر في الحقل Greatest Fear ثم اضغط على مفتاح TAB. اضغط على مفتاح السهم للأسفل عددًا صحيحًا من المرات لتحديد مصدر قواك السحرية Wizard Power Source، إذ ستضغط مرة واحدة لخيار العصا السحرية wand ومرتين لخيار التعويذة amulet وثلاث مرات لخيار الكرة البلورية crystal ball وأربع مرات لخيار المال money، ثم اضغط على مفتاح TAB. لاحظ أنه يجب أن تضغط على مفتاح السهم للأسفل مرة أخرى لكل خيار في نظام ماك macOS، وقد تحتاج إلى الضغط على مفتاح ENTER أيضًا بالنسبة لبعض المتصفحات. اضغط على مفتاح السهم الأيمن لتحديد إجابة سؤال RoboCop، واضغط عليه مرة واحدة للخيار 2 أو مرتين للخيار 3 أو ثلاث مرات للخيار 4 أو أربع مرات للخيار 5 أو اضغط على مفتاح المسافة لتحديد الخيار 1 المُحدَّد افتراضيًا، ثم اضغط على مفتاح TAB. اكتب تعليقًا إضافيًا في الحقل Additional Comments، ثم اضغط على مفتاح TAB. اضغط على مفتاح ENTER للنقر على زر الإرسال Submit. سينقلك المتصفح إلى صفحة أخرى بعد إرسال الاستمارة، حيث يجب اتباع رابط في هذه الصفحة للعودة إلى صفحة الاستمارة. قد تعمل المتصفحات الأخرى على أنظمة تشغيل مختلفة بطريقة مختلفة قليلًا عن الخطوات التي ذكرناها، لذا تأكد من أن هذه المجموعات من ضغطات المفاتيح تعمل على حاسوبك قبل تشغيل البرنامج. الخطوة الثانية: إعداد الإحداثيات حمّل مثال الاستمارة التي نزّلتها (الشكل السابق) في المتصفح من خلال الانتقال إلى الرابط autbor.com، واجعل شيفرتك البرمجية كما يلي: #! python3 # formFiller.py - ملء الاستمارة آليًا import pyautogui, time # منح المستخدم فرصةً لإنهاء السكربت # الانتظار حتى تحميل صفحة الاستمارة # ‫ملء حقل الاسم Name # ‫ملء حقل مخاوفك الكبرى Greatest Fear(s)‎ # ملء حقل مصدر قواك السحرية‫ Source of Wizard Powers # ‫ملء الحقل RoboCop # ‫ملء حقل التعليقات الإضافية Additional Comments # الضغط على زر الإرسال‫ Submit # الانتظار حتى تحميل صفحة الاستمارة # النقر على رابط إرسال رد آخر تحتاج الآن البيانات التي تريد إدخالها فعليًا في هذه الاستمارة، حيث قد تأتي هذه البيانات في العالم الحقيقي من جدول بيانات أو ملف نص عادي أو من موقع ويب، وقد تتطلب شيفرة برمجية إضافية لتحميلها في البرنامج، ولكننا سنكتب كل هذه البيانات ضمن متغير في مثالنا، لذا أضِف ما يلي إلى برنامجك: #! python3 # formFiller.py - ملء الاستمارة آليًا --snip-- formData = [{'name': 'Alice', 'fear': 'eavesdroppers', 'source': 'wand', 'robocop': 4, 'comments': 'Tell Bob I said hi.'}, {'name': 'Bob', 'fear': 'bees', 'source': 'amulet', 'robocop': 4, 'comments': 'n/a'}, {'name': 'Carol', 'fear': 'puppets', 'source': 'crystal ball', 'robocop': 1, 'comments': 'Please take the puppets out of the break room.'}, {'name': 'Alex Murphy', 'fear': 'ED-209', 'source': 'money', 'robocop': 5, 'comments': 'Protect the innocent. Serve the public trust. Uphold the law.'}, ] --snip– تحتوي القائمة formData على أربعة قواميس لأربعة أسماء مختلفة، ويحتوي كل قاموس على أسماء الحقول النصية كمفاتيحٍ له والردود كقيمٍ له. أخيرًا، نضبط المتغير PAUSE الخاص بوحدة PyAutoGUI للانتظار لمدة نصف ثانية بعد كل استدعاء دالة، ونذكّر المستخدم بالنقر على المتصفح لجعله النافذة النشطة. أضِف ما يلي إلى برنامجك بعد تعليمة إسناد قيمٍ إلى القائمة formData: pyautogui.PAUSE = 0.5 print('Ensure that the browser window is active and the form is loaded!') الخطوة الثالثة: البدء في كتابة البيانات سنكرّر حلقة for على كلٍّ من القواميس الموجودة في قائمة formData، ونمرّر القيم الموجودة في القاموس إلى دوال وحدة PyAutoGUI التي ستكتب فعليًا في الحقول النصية. أضِف الشيفرة البرمجية التالية إلى برنامجك: #! python3 # formFiller.py - ملء الاستمارة آليًا --snip-- for person in formData: # منح المستخدم فرصةً لإنهاء السكربت print('>>> 5-SECOND PAUSE TO LET USER PRESS CTRL-C <<<') ➊ time.sleep(5) --snip– يحتوي السكربت على توقف مؤقت لمدة خمس ثوانٍ ➊ كميزة أمانٍ صغيرة، مما يمنح المستخدم فرصةً للضغط على Ctrl-C (أو تحريك مؤشر الفأرة إلى الزاوية العلوية اليسرى من الشاشة لرفع استثناء FailSafeException) لإيقاف تشغيل البرنامج في حالة أنه يعمل شيئًا غير متوقع. أضِف ما يلي بعد الشيفرة البرمجية التي تنتظر إعطاء الصفحة وقتًا للتحميل: #! python3 # formFiller.py - ملء الاستمارة آليًا --snip-- ➊ print('Entering %s info...' % (person['name'])) ➋ pyautogui.write(['\t', '\t']) # ‫ملء حقل الاسم Name ➌ pyautogui.write(person['name'] + '\t') # ملء حقل مخاوفك الكبرى‫ Greatest Fear(s)‎ ➍ pyautogui.write(person['fear'] + '\t') --snip– نضيف استدعاء الدالة print()‎ من حينٍ لآخر لعرض حالة البرنامج في نافذته الطرفية لإعلام المستخدم بما يحدث ➊. حصلت الاستمارة على وقتها الكافي للتحميل، لذا نستدعي الدالة pyautogui.write(['\t', '\t'])‎ للضغط على مفتاح TAB مرتين والتركيز على حقل الاسم Name ➋، ثم نستدعي الدالة write()‎ مرة أخرى لإدخال السلسلة النصية في person['name']‎ ➌. نضيف المحرف ‎'\t'‎ إلى نهاية السلسلة النصية التي نمرّرها إلى الدالة write()‎ لمحاكاة الضغط على مفتاح TAB، مما ينقل تركيز لوحة المفاتيح إلى الحقل التالي وهو Greatest Fear(s)‎. يؤدي استدعاءٌ آخر للدالة write()‎ إلى كتابة السلسلة النصية في person['fear']‎ ضمن هذا الحقل ثم ينتقل إلى الحقل التالي في الاستمارة ➍. الخطوة الرابعة: التعامل مع قوائم التحديد وأزرار الاختيار تُعَد القائمة المنسدلة لسؤال "القوى السحرية Wizard Powers" وأزرار الاختيار الخاصة بحقل RoboCop أصعب في التعامل من الحقول النصية، حيث يمكنك النقر على هذه الخيارات باستخدام الفأرة من خلال معرفة إحداثيات x و y لكل خيار ممكن، ولكن من الأسهل استخدام مفاتيح الأسهم في لوحة المفاتيح لإجراء التحديد بدلًا من ذلك. أضِف ما يلي إلى برنامجك: #! python3 # formFiller.py - ملء الاستمارة آليًا --snip-- # ملء حقل مصدر قواك السحرية‫ Source of Wizard Powers ➊ if person['source'] == 'wand': ➋ pyautogui.write(['down', '\t'] , 0.5) elif person['source'] == 'amulet': pyautogui.write(['down', 'down', '\t'] , 0.5) elif person['source'] == 'crystal ball': pyautogui.write(['down', 'down', 'down', '\t'] , 0.5) elif person['source'] == 'money': pyautogui.write(['down', 'down', 'down', 'down', '\t'] , 0.5) # ملء الحقل‫ RoboCop ➌ if person['robocop'] == 1: ➍ pyautogui.write([' ', '\t'] , 0.5) elif person['robocop'] == 2: pyautogui.write(['right', '\t'] , 0.5) elif person['robocop'] == 3: pyautogui.write(['right', 'right', '\t'] , 0.5) elif person['robocop'] == 4: pyautogui.write(['right', 'right', 'right', '\t'] , 0.5) elif person['robocop'] == 5: pyautogui.write(['right', 'right', 'right', 'right', '\t'] , 0.5) --snip– تذكر أنك كتبتَ شيفرة برمجية لمحاكاة الضغط على مفتاح TAB بعد ملء حقل المخاوف الكبرى Greatest Fear(s)‎، وبالتالي سيؤدي الضغط على مفتاح السهم للأسفل إلى الانتقال إلى العنصر التالي في قائمة التحديد بعد التركيز على القائمة المنسدلة. يجب أن يرسل برنامجك عددًا من ضغطات مفاتيح السهم للأسفل قبل الانتقال إلى الحقل التالي اعتمادًا على القيمة الموجودة فيperson['source']‎، حيث إذا كانت قيمة مفتاح 'source' الموجودة في قاموس هذا المستخدم هي 'wand' ➊، فسنحاكي الضغط على مفتاح السهم للأسفل مرة واحدة (لتحديد القيمة Wand) والضغط على مفتاح TAB ➋. إذا كانت القيمة الموجودة في مفتاح 'source' هي 'amulet'، فسنحاكي الضغط على مفتاح السهم للأسفل مرتين والضغط على مفتاح TAB، وينطبق الشيء نفسه بالنسبة للإجابات المُحتمَلة الأخرى. يضيف الوسيط 0.5 في استدعاءات الدالة write()‎ توقفًا مؤقتًا لمدة نصف ثانية بين المفاتيح حتى لا يتحرك البرنامج بسرعة كبيرة في الاستمارة. يمكن تحديد أزرار الاختيار الخاصة بسؤال RoboCop باستخدام مفاتيح الأسهم إلى اليمين، أو إذا أردتَ تحديد الخيار الأول ➌، فاضغط على على شريط المسافة فقط ➍. الخطوة الخامسة: إرسال الاستمارة ثم الانتظار يمكنك ملء حقل التعليقات الإضافية Additional Comments باستخدام الدالة write()‎ من خلال تمرير person['comments']‎ كوسيطٍ لها. يمكنك كتابة مفتاح ‎'\t'‎ إضافي لنقل تركيز لوحة المفاتيح إلى الحقل التالي أو إلى زر الإرسال Submit. سيؤدي استدعاء الدالة pyautogui.press('enter')‎ إلى محاكاة الضغط على مفتاح ENTER وإرسال الاستمارة بعد التركيز على زر الإرسال Submit، ثم سينتظر برنامجك خمس ثوانٍ حتى تحميل الصفحة التالية. ستحتوي الصفحة الجديدة بعد تحميلها على رابط إرسال ردٍ آخر الذي سيوجّه المتصفح إلى صفحة استمارة جديدة فارغة، حيث خزّنا إحداثيات هذا الرابط بوصفها مجموعةً في المتغير submitAnotherLink في الخطوة الثانية، لذا مرّر هذه الإحداثيات إلى الدالة pyautogui.click()‎ للنقر على هذا الرابط. يمكن لحلقة for الخارجية الخاصة بالسكربت الاستمرار إلى التكرار التالي وإدخال معلومات الشخص التالي في الاستمارة عندما تكون الاستمارة الجديدة جاهزة. أكمِل برنامجك بإضافة الشيفرة البرمجية التالي: #! python3 # formFiller.py - ملء الاستمارة آليًا --snip-- # ‫ملء حقل التعليقات الإضافية Additional Comments pyautogui.write(person['comments'] + '\t') # الضغط على زر الإرسال‫ Submit من خلال الضغط على مفتاح Enter time.sleep(0.5) # Wait for the button to activate. pyautogui.press('enter') # الانتظار حتى تحميل صفحة الاستمارة print('Submitted form.') time.sleep(5) # النقر على رابط إرسال رد آخر pyautogui.click(submitAnotherLink[0], submitAnotherLink[1]) سيُدخِل البرنامج المعلومات الخاصة بكل شخص بعد انتهاء حلقة for الرئيسية، حيث يوجد في مثالنا أربعة أشخاص للإدخال فقط، ولكن إذا كان لديك 4000 شخص، فستوفر كتابة برنامج لإنجاز ذلك عليك الكثير من الوقت والكتابة. عرض مربعات الرسائل تميل جميع البرامج التي كتبناها حتى الآن إلى استخدام خرجٍ يحتوي على نصٍ عادي باستخدام الدالة print()‎ ودخلٍ يحتوي على نصٍ عادي باستخدام الدالة input()‎، ولكن ستستخدم برامج PyAutoGUI سطح المكتب بأكمله، إذ يُحتمَل فقدان النافذة النصية التي يعمل فيها برنامجك سواء كانت نافذة المحرّر Mu أو نافذة طرفية Terminal عندما ينقر برنامج PyAutoGUI الخاص بك ويتفاعل مع النوافذ الأخرى، مما يصعّب الحصول على الدخل والخرج من المستخدم عند إخفاء نوافذ المحرّر Mu أو نافذة الطرفية تحت نوافذ أخرى. يمكن حل هذه المشكلة باستخدام وحدة PyAutoGUI التي تقدّم مربعات رسائل منبثقة لتقديم إشعارات للمستخدم وتلقي الدخل منه، إذ توجد أربع دوال لمربعات الرسائل وهي: pyautogui.alert(text)‎: تعرض النص text وتحتوي على زر موافقة OK واحد. +pyautogui.confirm(text)‎: تعرض النص text وتحتوي على زر موافقة OK وزر إلغاء Cancel، وتعرض إما 'OK' أو 'Cancel' اعتمادًا على الزر الذي نقرنا عليه. pyautogui.prompt(text)‎: تعرض النص text وتحتوي على حقل نصي ليكتب المستخدم فيه، والذي تعيده كسلسلة نصية. pyautogui.password(text)‎: تماثل الدالة prompt()‎، ولكنها تعرض علامات نجمية على النص المُدخَل حتى يتمكّن المستخدم من إدخال معلومات حساسة مثل كلمة المرور. تحتوي هذه الدوال أيضًا على معاملٍ ثانٍ اختياري يقبل قيمة سلسلة نصية لاستخدامها بوصفها عنوانًا في شريط العنوان الخاص بمربع الرسالة. لن تعود هذه الدوال حتى ينقر المستخدم على الزر الموجود عليها، لذلك يمكن استخدامها أيضًا لإدخال فترات توقف مؤقت في برامج PyAutoGUI الخاصة بك. أدخِل مثلًا ما يلي في الصدفة التفاعلية: >>> import pyautogui >>> pyautogui.alert('This is a message.', 'Important') 'OK' >>> pyautogui.confirm('Do you want to continue?') # اضغط على زر الإلغاء 'Cancel' >>> pyautogui.prompt("What is your cat's name?") 'Zophie' >>> pyautogui.password('What is the password?') 'hunter2' تبدو مربعات الرسائل المنبثقة التي تنتجها السطور السابقة كما يلي: النوافذ من أعلى اليسار إلى أسفل اليمين هي: النوافذ التي تنشئها الدوال alert()‎ و confirm()‎ و prompt()‎ وpassword()‎ يمكن استخدام هذه الدوال لتقديم إشعارات أو طرح أسئلة على المستخدم أثناء تفاعل باقي البرنامج مع الحاسوب من خلال الفأرة ولوحة المفاتيح. اطّلع على التوثيق الكامل عبر الإنترنت للوحدة PyMsgBox على موقعها الرسمي. مشاريع للتدريب حاول كتابة البرامج التي تؤدي المهام التي سنوضّحها فيما يلي لكسب خبرة عملية أكبر. برنامج لإبقاء الحالة "مشغول" على برنامج المراسلة الفورية تحدّد العديد من برامج المراسلة الفورية ما إذا كنت في وضع السكون أو كنت بعيدًا عن حاسوبك من خلال اكتشاف عدم وجود حركة للفأرة خلال فترة زمنية معينة مثل 10 دقائق. قد تكون بعيدًا عن حاسوبك ولكنك لا تريد أن يرى الآخرون حالة المراسلة الفورية الخاصة بك وهي في وضع السكون، لذا اكتب برنامجًا لتحريك مؤشر الفأرة قليلًا كل 10 ثوانٍ، حيث يجب أن تكون الحركة صغيرة وغير متكررة بدرجة كافية حتى لا تعترض طريقك إذا أردتَ استخدام حاسوبك أثناء تشغيل السكربت. استخدام الحافظة Clipboard لقراءة حقل نصي يمكنك إرسال ضغطات المفاتيح إلى الحقول النصية في التطبيق باستخدام الدالة pyautogui.write()‎، ولكن لا يمكنك استخدام وحدة PyAutoGUI وحدها لقراءة النص الموجود فعليًا ضمن الحقل النصي، لذا يمكن أن تساعد الوحدة Pyperclip في ذلك. استخدم الوحدة PyAutoGUI للحصول على نافذة محرّر نصوص مثل المحرّر Mu أو المفكرة Notepad، وإحضارها إلى مقدمة الشاشة من خلال النقر عليها، ثم النقر داخل الحقل النصي، وإرسال مفتاح التشغيل السريع CTRL-A أو ‎ -A لتحديد الكل وإرسال مفتاح التشغيل السريع Ctrl-C أو ‎ -C للنسخ إلى الحافظة، ويمكن لسكربت بايثون بعد ذلك قراءة نص الحافظة من خلال تشغيل التعليمتين import pyperclip و pyperclip.paste()‎. اكتب برنامجًا يتبع هذا الإجراء لنسخ النص من الحقول النصية في النافذة، لذا استخدم الدالة pyautogui.getWindowsWithTitle('Notepad')‎ (أو أي محرّر نصوص تختاره) للحصول على كائن Window. يمكن لسمات top و left لكائن Window أن تخبرك بمكان هذه النافذة، وسيضمن التابع activate()‎ وجودها في مقدمة الشاشة. يمكنك بعد ذلك النقر على الحقل النصي الرئيسي لمحرّر النصوص من خلال إضافة 100 أو 200 بكسل مثلًا إلى قيم السمات top و left باستخدام التابع pyautogui.click()‎ لنقل تركيز لوحة المفاتيح إلى هذا الحقل، ثم استدعِ الدالتين pyautogui.hotkey('ctrl', 'a')‎ و pyautogui.hotkey('ctrl', 'c')‎ لتحديد النص بأكمله ونسخه إلى الحافظة. أخيرًا، استدعِ الدالة pyperclip.paste()‎ لاسترداد النص من الحافظة ولصقه في برنامج بايثون. يمكنك بعد ذلك استخدام هذه السلسلة النصية كما تريد، ولكن مرّرها إلى الدالة print()‎ حاليًا. لاحظ أن دوال النافذة الخاصة بوحدة PyAutoGUI تعمل فقط على نظام ويندوز بدءًا من الإصدار 1.0.0 من وحدة PyAutoGUI، ولن تعمل على نظام ماك macOS أو لينكس. بوت المراسلة الفورية تستخدم برامج المراسلة الفورية بروتوكولاتٍ خاصة تصعّب كتابة وحدات بايثون التي يمكنها التفاعل مع هذه البرامج، ولكن لا يمكن لهذه البروتوكولات الخاصة منعك من كتابة أداة أتمتة لواجهة المستخدم الرسومية. يحتوي تطبيق واتس أب Whatsapp على شريط بحث يتيح لك إدخال اسم مستخدم في قائمة أصدقائك وفتح نافذة مراسلة عند الضغط على مفتاح ENTER، حيث ينتقل تركيز لوحة المفاتيح إلى النافذة الجديدة آليًا، وتمتلك تطبيقات المراسلة الفورية الأخرى طرقًا مشابهة لفتح نوافذ الرسائل الجديدة. اكتب برنامجًا يرسل رسالة إشعار آليًا إلى مجموعة مختارة من الأشخاص في قائمة أصدقائك، وقد يضطر برنامجك إلى التعامل مع حالات استثنائية مثل ظهور نافذة الدردشة في إحداثيات مختلفة على الشاشة، أو ظهور مربعات التأكيد التي تقاطع رسائلك. يجب على برنامجك التقاط لقطات شاشة لتوجيه تفاعل واجهة المستخدم الرسومية واعتماد طرقٍ لاكتشاف متى لا تُرسَل ضغطات المفاتيح الافتراضية. ملاحظة: قد ترغب في إعداد بعض الحسابات التجريبية الوهمية حتى لا ترسل رسائل غير مرغوب فيها إلى أصدقائك الحقيقيين عن طريق الخطأ أثناء كتابة هذا البرنامج. الخلاصة تتيح لك أتمتة واجهة المستخدم الرسومية باستخدام وحدة pyautogui التفاعل مع التطبيقات الموجودة على حاسوبك من خلال التحكم في الفأرة ولوحة المفاتيح، حيث يُعَد هذا النهج مرنًا بما يكفي لفعل أيّ شيء يمكن للمستخدم تطبيقه، ولكن يتمثّل جانبه السلبي في أن هذه البرامج لا تستطيع رؤية ما تنقر عليه أو تكتبه. حاول التأكد من أن برامج أتمتة واجهة المستخدم الرسومية عند كتابتها ستتعطّل بسرعة عند إعطائها تعليمات سيئة، إذ قد يكون تعطل البرنامج أمرًا مزعجًا، ولكنه أفضل بكثير من استمرار البرنامج مع وجود الخطأ. يمكنك تحريك مؤشر الفأرة على الشاشة ومحاكاة نقرات الفأرة وضغطات المفاتيح واختصارات لوحة المفاتيح باستخدام وحدة PyAutoGUI التي يمكنها أيضًا التحقق من الألوان على الشاشة، ويمكنها أن تزوّد برنامج أتمتة واجهة المستخدم الرسومية الخاص بك بفكرة كافية عن محتويات الشاشة لمعرفة خروجه عن المسار الصحيح أم لا، ويمكنك إعطاء الوحدة PyAutoGUI لقطة شاشة والسماح لها بمعرفة إحداثيات المنطقة التي تريد النقر عليها. يمكنك الجمع بين جميع ميزات PyAutoGUI لأتمتة أيّ مهمة متكررة على حاسوبك. قد تكون مشاهدة مؤشر الفأرة يتحرك من تلقاء نفسه ورؤية النص يظهر على الشاشة آليًا أمرًا مملًا للغاية، ولكن يوجد شعور معين بالرضا يأتي من رؤية كيف أنقذك ذكاؤك من إنجاز المهام المملة. ترجمة -وبتصرُّف- للمقال Controlling the Keyboard and Mouse with GUI Automation لصاحبه Al Sweigart. اقرأ أيضًا المقال السابق: معالجة الصور باستخدام لغة بايثون Python الأدوات المستخدمة في بناء الواجهات الرسومية في بايثون جدولة المهام وتشغيل برامج أخرى باستخدام لغة بايثون واجهات المستخدم الرسومية في بايثون باستخدام TKinter
×
×
  • أضف...