لوحة المتصدرين
المحتوى الأكثر حصولًا على سمعة جيدة
المحتوى الأعلى تقييمًا في 03/16/21 في كل الموقع
-
الإصدار 1.0.0
13395 تنزيل
مع الغياب شبه التام لأي مصادر مفيدة باللغة العربية عن مجالات الخصوصية والحماية والأمان الرقمي وتأمين الأجهزة الشخصية، جاء هذا الكتاب ليكون شارحًا لمفاهيم الحماية والأمان التي يحتاج إليها كل من يعيش في هذا العصر الرقمي ويستعمل مختلف الأجهزة الرقمية في شتى مجالات الحياة وذلك لتأمين نفسه وحماية بياناته التي يضعها في تلك الأجهزة. إنّ الأمان الرقمي موضوعٌ مهم للحديث عنه وليس شيئًا رفاهيًا أو تكميليًا، خصوصًا مع اطّراد عدد المستخدمين الجدد مع عدد انتهاكات واختراقات الأمان والخصوصية التي تحصل كلّ يوم. إنّ هذا الكتاب موجّهٌ بالدرجة الأولى إلى عموم من يستخدم الحواسيب والأجهزة الذكية ويهدف إلى توعيتهم بأهم المسائل المتعلقة بالأمان الرقمي، ولا يخلو من مواضيع ومعلومات مفيدة ومتقدمة حتّى للخبراء والمتخصصين في المجال، إذ أنّ هذا الكتاب ما هو إلّا محاولة لتحصين المستخدمين ولفت انتباههم إلى مجال الأمان الرقمي وطرق حماية أنفسهم في العالم الرقمي، ولا يعد الكتاب مرجعًا شاملًا لكلّ شيء في المجال. يبدأ الكتاب بعرض المفاهيم الأساسية التي يجب أن يمتلكها أيّ قارئ للكتاب، وهي مفاهيمٌ تعتمد عليها الكثير من الفصول الأخرى في الكتاب فلا غنى عنها بحالٍ من الأحوال، ثم ينتقل الكتاب إلى الحديث عن الوعي وأهميته، وقد قدّمنا موضوع الوعي على غيره لأنّه مبدأٌ عام يُمكن تطبيقه في مختلف مجالات الحماية الرقمية وليس شرحًا لطريقة تثبيت برنامج أو إضافة مثلًا، كما أنّه أهم طريقة لحماية المُستخدم نفسَه. ويأتي بعد هذين الفصلين مختلف الفصول التي تشرح اختيار خدماتٍ معينة أو طريقة تأمين أجهزة وأنظمة معينة. يجد القارئ في كلّ فصلٍ من هذه الفصول شرحًا للمفهوم المُراد تأمينه قبل الشروع بطريقة حمايته وتأمينه ولذلك يمكنك قراءة هذه المفاهيم بالترتيب الذي تريد بحسب حاجتك إن أردت ذلك رغم أنه ينصح بقراءتها وفق ترتيبها المتسلسل. هذا الكتاب مرخص بموجب رخصة المشاع الإبداعي Creative Commons «نسب المُصنَّف - غير تجاري - الترخيص بالمثل 4.0». يمكنك قراءة فصول الكتاب على شكل مقالات من هذه الصفحة، «الأمان الرقمي»، أو مباشرة مما يلي: الفصل الأول: لماذا يجب أن نحافظ على أماننا الرقمي؟ الفصل الثاني: مفاهيم تأسيسية عن الأمان الرقمي الفصل الثالث: الوعي في العالم الرقمي الفصل الرابع: اختيار العتاد والبرامج في العالم الرقمي الفصل الخامس: اختيار الخدمات والمزودات في العالم الرقمي الفصل السادس: تأمين الأشياء الأساسية المحيطة بك في العالم الرقمي الفصل السابع: النسخ الاحتياطي وحفظ البيانات في العالم الرقمي الفصل الثامن: التشفير واستعمالاته في العالم الرقمي الفصل التاسع: كلمات المرور: كيفية حفظها واستعمالها في العالم الرقمي الفصل العاشر: تأمين متصفحات الويب في العالم الرقمي الفصل الحادي عشر: الحماية من مواقع الإنترنت في العالم الرقمي الفصل الثاني عشر: ما يلزم معرفته عند الشراء والدفع عبر الإنترنت الفصل الثالث عشر: تأمين الهاتف المحمول في العالم الرقمي الفصل الرابع عشر: كيف تعرف أنك اخترقت في العالم الرقمي وماذا تفعل حيال ذلك؟ الفصل الخامس عشر: مواضيع متقدمة في الأمان الرقمي2 نقاط -
يوجد طريقتين لإتاحة الصلاحيات على المشروع: إما أن يتم منح الصلاحيات للخادم، وهي الطريقة المفضلة عند أغلب المبرمجين وأيضاً التي ينصح بها التوثيق الرسمي للارافيل. فمثلاً باعتبار www هو الخادم يمكننا منح صلاحيات الكتابة كالتالي: sudo chown -R www-data:www-data /path/to/your/laravelproject وبذلك يمتلك خادم الويب جميع الملفات ضمن مجلد المشروع، وهنا ستواجه بعض المشاكل في حال كنت تستخدم FTP من طرف المستخدم لأنه الاتصال عبر FTP سيكون مسجل دخول وبدوره لا يملك الملفات الموجودة ضمن المشروع. لذلك يجب عليك بعدها إضافة المستخدم إلى نفس المجموعة التي تحوي الخادم كالتالي: sudo usermod -a -G www-data ubuntu وبعدها يمكن إعطاء الصلاحيات على الملفات جميعها كالتالي: sudo find /path/to/your/laravelproject -type f -exec chmod 644 {} \; وصلاحيات المجلدات: sudo find /path/to/your/laravelproject -type d -exec chmod 755 {} \; أما الطريقة الثانية، فهي عن طريق منح الصلاحيات للمستخدم نفسه. والذي يسهّل آلية العمل عن طريق منح الصلاحيات مباشرةً للمستخدم. ولتنفيذ ذلك أولاً نتوجه إلى مسار مجلد المشروع: cd /var/www/html/laravelproject ثم نقوم بتنفيذ الأمر التالي: sudo chown -R $USER:www-data . ثم نقوم بإعطاء الصلاحيات للمستخدم على الملفات والمجلدات: sudo find . -type f -exec chmod 664 {} \; sudo find . -type d -exec chmod 775 {} \; وأخيراً، يجب أيضاً إعطاء الصلاحيات للقراءة والكتابة على storage و cache ضمن لارافيل كالتالي: sudo chgrp -R www-data storage bootstrap/cache sudo chmod -R ug+rwx storage bootstrap/cache2 نقاط
-
يجب أولاً تحميل المكتبة عن طريق npm كالتالي: npm install vuetify إضافة السطر التالي في تروسية ملف /resources/views/app.blade.php لكي يتم التعرف على الأيقونات الخاصة ب vuetify: <link href="https://cdn.jsdelivr.net/npm/@mdi/font@4.x/css/materialdesignicons.min.css" rel="stylesheet"> ثم في ملف /resources/js/app.js نضيف التالي فقط: import Vuetify from 'vuetify'; import 'vuetify/dist/vuetify.min.css'; Vue.use(Vuetify) مع التأكد بإضافتهم بعد Vue الأساسية بحيث يصبح شكل الملف كالتالي: require('./bootstrap'); import Vue from 'vue'; import Vuetify from 'vuetify'; import { InertiaApp } from '@inertiajs/inertia-vue'; import { InertiaForm } from 'laravel-jetstream'; import PortalVue from 'portal-vue'; import 'vuetify/dist/vuetify.min.css'; Vue.mixin({ methods: { route } }); Vue.use(InertiaApp); Vue.use(InertiaForm); Vue.use(PortalVue); Vue.use(Vuetify) const app = document.getElementById('app'); new Vue({ vuetify: new Vuetify(), render: (h) => h(InertiaApp, { props: { initialPage: JSON.parse(app.dataset.page), resolveComponent: (name) => require(`./Pages/${name}`).default, }, }), }).$mount(app); ويجب أيضاً التأكد من ملف webpack.mix.js أنه يحوي vuetify loader كالتالي: const mix = require('laravel-mix') const VuetifyLoaderPlugin = require('vuetify-loader/lib/plugin') mix .js('resources/js/app.js', 'public/js') .webpackConfig({ plugins: [ new VuetifyLoaderPlugin() ], }) .browserSync('tb8.test');2 نقاط
-
تحتاج إلى التعديل على ملف الإعدادات env. لكن أولا قم بتنفيذ الأمر: docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' <container_name_or_container_ID> سيقوم الأمر بإرجاع عنوان IP لل mysql container ستقوم بنسخه ووضعه في ملف env. DB_HOST=<ip_address_returned> بعد ذلك قم بتنفيذ امر التهجير: sail artisan migrate2 نقاط
-
لا لا تقف عليك ان تواصل ما بدأته من اللارافيل حتى لا تقطع حبل افكارك و لربما عندما تعود إلى اللارافيل من جديد ستحتاج إلى أن تبدأ من الصفر لأنك حتى الان لم تقم بتثبيت ما تعلمته من خلال التطبيق العملي بصورة كافية هذا بالاضافة إلى أنه بما أنك وصلت إلى الviews فهذا يعني أنك تمتلك اساساً جيداً في لغة php ولست بحاجة إلى العودة إلى الأساسيات لذلك قم بمتابعة اللارافيل حتى النهاية ثم بعد ذلك يمكنك العودة الى التحديث الجديد.2 نقاط
-
لا يمكننا فقط إسناد معالجات للأحداث من خلال جافاسكربت، ولكن يمكننا أيضا توليد أحداث مخصّصة. يُمكن استعمال الأحداث المخصّصة لإنشاء "مكوّنات رسوميّة". على سبيل المثال، يمكن للعنصر الجذر في قائمةٍ تعمل بواسطة جافاسكربت افتعال أحداثٍ تُنبئ بما يحصل مع القائمة: open (عند فتح القائمة)، select (عند تحديد عنصر) وهكذا. يمكن أن تنصت شيفرة أخرى لهذه الأحداث وتراقب ماذا يحصل مع القائمة. لا يمكننا فقط توليد أحداث جديدة كليّا، نخترعها لأغراضنا الخاصّة، ولكن يمكن أيضا توليد اﻷحداث المضمّنة، مثل click و mousedown إلى غير ذلك. قد يساعد ذلك عند إجراء الاختبارات الآليّة. باني الأحداث تشكّل أصناف الأحداث المضمّنة سُلّميّة مشابهة لسلّميّة أصناف عناصر DOM. يكون الجذر فيها هو الصنف المُضمّن Event. ويمكننا إنشاء كائنات منها بهذه الطريقة: let event = new Event(type[, options]); الوسائط: type -- نوع الحدث، ويكون سلسلة نصيّة مثل "click" أو إذا كان خاصّا بنا مثل "my-event". options -- كائن بخاصيتين اختياريتين: bubbles: true/false -- إذا كانت true، فإنّ الحدث ينتشر نحو الأعلى. cancelable: true/false: إذا كانت true، فمن الممكن منع "الفعل الافتراضي". سنرى لاحقًا ما يعني ذلك للأحداث المخصّصة. تكون قيمة كلتيهما false افتراضيّا: {bubbles: false, cancelable: false}. dispatchEvent بعد إنشاء كائن الحدث، نستطيع أن "نجريه" على عنصرٍ ما بواسطة الاستدعاء elem.dispatchEvent(event). تستجيب المعالجات له حينها كما لو كان حدث متصفّح عادي. وإذا كان الحدث قد أنشئ وله الراية bubbles، فإنّه ينتشر نحو الأعلى. ابتُدِئ الحدث click في المثال أدناه من خلال جافاسكربت. يعمل المعالج بنفس الطريقة كما لو أن الزرّ قد نُقر بالفعل: <button id="elem" onclick="alert('Click!');">Autoclick</button> <script> let event = new Event("click"); elem.dispatchEvent(event); </script> See the Pen JS-p2-dispatch-events -ex1 by Hsoub (@Hsoub) on CodePen. ملاحظة: event.isTrusted هناك طريقة لمعرفة إذا كان الحدث "حقيقيّا" من المستخدم أو مولّدًا بواسطة سكربت. تكون قيمة الخاصيّة event.isTrusted هي true للأحداث التي تأتي من أفعال حقيقيّة للمستخدم و تكون false للأحداث المولّدة بواسطة سكربت. مثال عن الانتشار نحو الأعلى يمكننا إنشاء حدث منتشر نحو الأعلى باسم "hello" والتقاطه في document. كلّ ما نحتاجه هو إعطاء bubbles القيمة true: <h1 id="elem">Hello from the script!</h1> <script> // ... document الالتقاط على مستوى document.addEventListener("hello", function(event) { // (1) alert("Hello from " + event.target.tagName); // Hello from H1 }); // elem الإرسال من ... let event = new Event("hello", {bubbles: true}); // (2) elem.dispatchEvent(event); // و يظهر الرسالة document سيشتغل المعالج المسند إلى </script> See the Pen JS-p2-dispatch-events -ex2 by Hsoub (@Hsoub) on CodePen. ملاحظات: يجب أن نستخدم addEventListener لأحداثنا المخصصة، لأن on<event> توجد فقط للأحداث المضمّنة، فلا تعمل document.onhello مثلا. يجب وضع bubbles:true، وإلا فلن ينتشر الحدث نحو الأعلى. آلية الانتشار نحو الأعلى هي نفسها للأحداث المُضمّنة (click) والمخصصة (hello). هناك أيضا مرحلتا الانتشار نحو الأعلى والانتشار نحو الأسفل. MouseEvent و KeyboardEvent وغيرهما هذه قائمة قصيرة لأصناف أحداث واجهة المستخدم مأخوذة من مواصفة أحداث واجهة المستخدم: UIEvent FocusEvent MouseEvent WheelEvent KeyboardEvent … ينبغي أن نستخدمها عوضا عن new Event إذا أردنا إنشاء هذه الأحداث. على سبيل المثال، new MouseEvent("click"). يمكّن الباني المناسب من تحديد خاصيّات قياسية تتعلّق بنوع الحدث ذاك. مثل clientX/clientY لأحداث المؤشر: let event = new MouseEvent("click", { bubbles: true, cancelable: true, clientX: 100, clientY: 100 }); alert(event.clientX); // 100 يرجى التنبه: لا يتيح الباني العام Event ذلك. لنجرّب: let event = new Event("click", { bubbles: true, // cancelable و bubbles فقط cancelable: true, // Event تعملان في الباني clientX: 100, clientY: 100 }); alert(event.clientX); // تُهمل الخاصّية غير المعروفة ،undefined في الحقيقة، يمكننا الاحتيال على ذلك بإسناد event.clientX=100 مباشرة بعد إنشائه. فيؤول الأمر إلى المناسبة والالتزام بالقواعد. يكون نوع الأحداث التي يولّدها المتصفّح صحيحًا على الدوام. توجد القائمة الشاملة لمختلف أحداث واجهة المستخدم في المواصفة، على سبيل المثال، MouseEvent. الأحداث المخصصة بالنسبة لأنواع الأحداث الخاصّة بنا والجديدة كليّا مثل "hello" علينا أن نستخدم new CustomEvent. فنيّا، CustomEvent هي نفس Event لكن مع استثناء وحيد. في الوسيط الثاني (object) يمكننا إضافة خاصيّة أخرى detail من أجل أيّة معلومات مخصصّة نودّ تمريرها مع الحدث. على سبيل المثال: <h1 id="elem">Hello for John!</h1> <script> // تأتي المزيد من التفاصيل مع الحدث إلى المعالج elem.addEventListener("hello", function(event) { alert(event.detail.name); }); elem.dispatchEvent(new CustomEvent("hello", { detail: { name: "John" } })); </script> See the Pen JS-p2-dispatch-events -ex3 by Hsoub (@Hsoub) on CodePen. يمكن أن تحوي هذه الخاصيّة أي معطيات. في الحقيقة، من الممكن أن نعمل بدونها، لأننا نستطيع أن نسند أيّ خاصيّة إلى كائن new Event عاديّ بعد إنشائه. لكن CustomEvent تزوّده بحقل detail الخاص لتفادي التعارض مع خاصيّات الحدث الأخرى. إلى جانب ذلك، يبيّن صنف الحدث "أيّ نوع حدث" هو، وإذا كان الحدث مخصّصا، فينبغي استخدام CustomEvent لنكون فقط واضحين بخصوصه. ()event.preventDefault لدى العديد من أحداث المتصفّح "أفعال افتراضيّة"، كالانتقال إلى رابط، بداية تحديد، وما إلى ذلك. أمّا الأحداث المخصّصة الجديدة، فليس لديها بالطبع أيّة أفعال افتراضيًّة، لكن قد تكون للشيفرة التي ترسل مثل هذه الأحداث خُططًا خاصّة تقوم بها بعد افتعال الحدث. باستدعاء event.preventDefault() ، يمكن لمعالج الحدث أن يرسل إشارة بإنّ تلك الأفعال المخطّط لها يجب أن تُلغى. في تلك الحالة يعيد الاستدعاء elem.dispatchEvent(event) القيمة false. وتعلم الشيفرةُ التي أرسلت الحدث بأنّ عليها ألا تكمل. لنرى مثالًا تطبيقيّا لذلك -- أرنب متخفّي (قد يكون قائمة أيضا منغلقة أو أيّ شيئ آخر). يمكن أن ترى في الأسفل الأرنب rabbit# وعليه الدالة hide() التي ترسل الحدث "hide"، لتعلم جميع الأطراف المعنيّة بأنّ الأرنب سيختفي. يمكن لأي معالج أن يستمع لذلك الحدث بواسطة rabbit.addEventListener('hide',...) ، وإن تتطلّب الأمر، يمكنه إلغاء الفعل باستخدام event.preventDefault(). فعندها لن يختفي الأرنب: <pre id="rabbit"> |\ /| \|_|/ /. .\ =\_Y_/= {>o<} </pre> <button onclick="hide()">Hide()</button> <script> function hide() { let event = new CustomEvent("hide", { cancelable: true // preventDefault بدون تلك الراية لا يعمل }); if (!rabbit.dispatchEvent(event)) { alert('The action was prevented by a handler'); } else { rabbit.hidden = true; } } rabbit.addEventListener('hide', function(event) { if (confirm("Call preventDefault?")) { event.preventDefault(); } }); </script> See the Pen JS-p2-dispatch-events -ex4 by Hsoub (@Hsoub) on CodePen. يرجى التنبه هنا: يجب أن يكون للحدث الراية cancelable: true، وإلّا فسيُهمل event.preventDefault() . تعمل الأحداث التي داخل أحداث أخرى بشكل متزامن تُعالَج الأحداث عادةً على شكل طابور. بعبارة أخرى، إذا كان المتصفّح يقوم بمعالجة onclick ووقع حدث جديد، كتحريك الفأرة مثلًا، فإن معالجته تُصفّ في طابور الانتظار، وتُستدعى المعالجات المتعلّقة بـ mousemove بعد الانتهاء من معالجة onclick. الاستثناء الجدير بالانتباه هنا هو عندما ينشأ حدث من داخل حدث آخر، بواسطة dispatchEvent مثلا. تُعالج تلك الأحداث فورًا: تُستدعى معالجات الحدث الجديد ثم تستمر معالجة الحدث الحالية. على سبيل المثال، في الشيفرة أدناه يُفتعل الحدث menu-open خلال الحدث onclick. ويُعالَج فورًا دون انتظار معالج onclick من الانتهاء: <button id="menu">Menu (click me)</button> <script> menu.onclick = function() { alert(1); menu.dispatchEvent(new CustomEvent("menu-open", { bubbles: true })); alert(2); }; // يفتعل بين 1 و 2 document.addEventListener('menu-open', () => alert('nested')); </script> See the Pen JS-p2-dispatch-events -ex5 by Hsoub (@Hsoub) on CodePen. يكون ترتيب المخرجات كما يلي: 1 -› الحدث الداخلي -› 2. يُرجى التنبّه إلى أنّ الحدث الداخليّ menu-open قد تم ألتقاطه على مستوى document. يتمّ انتشار الحدث الداخلي ومعالجته قبل عودة المعالجة إلى الشيفرة الخارجية (onclick). لا يخصّ هذا فقط dispatchEvent، بل هناك حالات أخرى. إذا استدعى معالجٌ توابعَ تفتعل أحداثًا أخرى، فإنّها تُعالج تزامنيًّا أيضًا، بشكلٍ متداخل. لكن لنفترض أنّنا لا نريد ذلك. نريد أن تتمّ معالجة onclick أوّلًا، باستقلالٍ عن menu-open أو غيره من اﻷحداث المتداخلة. يمكننا عندها إمّا أن نضع dispatchEvent (أو نداءَ افتعال أحداثٍ آخر) في آخر onclick، أو ربّما أفضل، أن نلفّه بـ setTimeout منعدمة التأخير: <button id="menu">Menu (click me)</button> <script> menu.onclick = function() { alert(1); setTimeout(() => menu.dispatchEvent(new CustomEvent("menu-open", { bubbles: true }))); alert(2); }; document.addEventListener('menu-open', () => alert('nested')); </script> See the Pen JS-p2-dispatch-events -ex6 by Hsoub (@Hsoub) on CodePen. تعمل الآن dispatchEvent لا تزامنيًّا بعد الانتهاء من تنفيذ الشيفرة الحاليّة، بما في ذلك menu.onclick، وتصير بذلك معالجات الأحداث منفصلة تمامًا. ويصير ترتيب المخرجات كالتالي: 1 -> 2 -> الحدث الداخلي. الملخص لتوليد حدثٍ من خلال الشيفرة، نحتاج أوّلًا أن ننشئ كائن حدث. يقبل الباني العام Event(name, options) اسمًا للحدث والكائن options مع الخاصّيّتين: bubbles: true إذا كان يجب أن ينتشر الحدث نحو اﻷعلى. cancelable: true إذا كان يجب أن يعمل event.preventDefault(). يقبل بانو الأحداث اﻷصليّة الآخرون مثل MouseEvent و KeyboardEvent و ما إلى ذلك، خاصّيّات مختصّة بنوع الحدث ذلك. على سبيل المثال، clientX لأحداث المؤشر. ينبغي أن نستخدم الباني CustomEvent للأحداث المخصّصة. فلديه خيار إضافيّ اسمه detail، يمكن أن نسند إليه المعطيات المختصّة بالحدث. وبذلك يمكن لجميع المعالجات الوصول إليها بواسطة event.detail. بالرغم من الإمكانيّة التقنيّة لتوليد أحداث المتصفّح مثل click أو keydown، فينبغي استخدامها بحذرٍ شديد. لا ينبغي أن نعمد إلى توليد أحداث المتصفّح، إذ يعدّ ذلك طريقة مبتذلة لتشغيل المعالجات. وهي هندسة سيّئة في أغلب اﻷحيان. يمكن أن تُولّد اﻷحداث اﻷصليّة: كطريقة مبتذلة لجعل مكتبات الطرف الثالث تعمل كما يجب، إذا لم تكن توفّر وسائل أخرى للتفاعل معها. عند إجراء الاختبارات الآليّة، مثل "النقر على الزرّ" من خلال السكربت لمعرفة ما إذا كانت الواجهة تستجيب بشكل صحيح. تُولَّد الأحداث المخصّصة بأسمائنا الخاصّة لأغراض هندسيّة في الغالب، للإشارة إلى ما يحصل داخل القوائم و الدوّارات وما إلى ذلك. ترجمة -وبتصرف- للمقال Dispatching custom events من سلسلة Browser: Document, Events, Interfaces لصاحبها Ilya Kantor1 نقطة
-
لنتعمّق في المزيد من التفاصيل حول اﻷحداث التي تقع عندما تتحرّك الفأرة بين العناصر. اﻷحداث mouseover/mouseout و relatedTarget يقع الحدث mouseover عندما يأتي مؤشّر الفأرة إلى عنصر ما، ويقع mouseout عندما يغادره. تتميّز هذه اﻷحداث بأنّ لديها الخاصيّة relatedTarget. تُكمِّل هذه الخاصيّةُ target. عندما تغادر الفأرة عنصرا ما نحو عنصر آخر، يصير إحدى هذه العناصر target، ويصير الآخر relatedTarget. بالنسبة للحدث mouseover: event.target هو العنصر الذي أتت إليه الفأرة. event.relatedTarget هو العنصر الذي أتت منه الفأرة (relatedTarget -> target). والعكس بالنسبة للحدث mouseout: event.target هو العنصر الذي غادرته الفأرة. event.relatedTarget هو العنصر الذي غادرت نحوه الفأرة، وصار تحت المؤشّر (target -> relatedTarget) في المثال التالي (يمكن مشاهدته من هنا)، يشكّل كلُّ وجهٍ وتقاسيمُه عناصر منفصلة. عند تحريك الفأرة، يمكنك في المساحة النصيّة أسفله مشاهدة الأحداث التي تقع. يرافق كلَّ حدث المعلوماتُ المتعلّقة بكلٍّ من target وrelatedTarget. See the Pen JS-p2-mousemove-mouseover-mouseout-mouseenter-mouseleave-ex01 by Hsoub (@Hsoub) on CodePen. تنبيه: يمكن أن يأخذ relatedTarget القيمة null يمكن للخاصيّة relatedTarget أن تكون null. هذا أمر عاديّ ويعني فقط أنّ الفأرة لم تأتِ من عنصر آخر، ولكن من خارج النافذة. أو أنّها قد غادرت النافذة. تخطي العناصر يقع الحدث mousemove عندما تتحرّك الفأرة. ولكنّ ذلك لا يعني أنّ كلَّ بكسل يؤدّي إلى حدث. يرصد المتصفّح الفأرة من وقت لآخر. وإذا لاحظ تغيّرات، فإنّه يعمل على وقوع اﻷحداث. هذا يعني لو حرّك المستخدم الفأرة بسرعة كبيرة فإنّ بعض عناصر DOM قد تُتخطّى: إذا تحرّكت الفأرة بسرعة كبيرة من العنصر #FROM إلي العنصر #TO كما هو مبيّن أعلاه، فإنّ عناصر <div> التي بين هذين العنصرين (أو بعضها) قد تُتخطّى. قد يقع الحدث mouseout على #FROM ثم مباشرة على #TO. يساعد هذا على تحسين اﻷداء، لأنّه قد تكون هناك الكثير من العناصر البينيّة، ولا نريد حقًّا معالجة الدخول والخروج في كلٍّ منها. في المقابل، يجب أن ندرك دائما أن مؤشّر الفأرة لا "يزور" جميع العناصر في طريقه، بل قد "يقفز". فمثلا، من الممكن أن يقفز المؤشّر إلى وسط الصفحة مباشرة قادما من خارج الصفحة. في هذه الحالة، تكون قيمة relatedTarget هي null، لأنّه قد أتى من "لا مكان": يمكنك رؤية ذلك "حيًّا" في المثال التالي أو من منصّة الاختبار التي من هنا: See the Pen JS-p2-mousemove-mouseover-mouseout-mouseenter-mouseleave-ex02 by Hsoub (@Hsoub) on CodePen. يوجد هناك في HTML عنصران متداخلان: العنصر <div id="child"> موجود داخل العنصر <div id="parent">. إذا حرّكتَ الفأرة بسرعة فوقهما، فربّما يقع الحدث على العنصر div الابن فقط، أو ربّما على اﻷب، أو ربّما لن تكون هناك أحداث مطلقا. قم أيضا بتحريك المؤشّر إلى داخل العنصر div الابن، ثم حرّكه بسرعة نحو اﻷسفل عبر العنصر اﻷب. إذا كانت الحركة سريعة كفاية، فسيُتجاهل العنصر اﻷب. ستعبر الفأرة العنصر اﻷب دون الانتباه له. ملاحظة: إذا وقع mouseover، فلابدّ أن يكون هناك mouseout في حالة تحرّك الفأرة بسرعة، فقد تُتجاهل العناصر البينيّة، لكن هناك شيء مؤكّد: إذا دخل المؤشّر "رسميًّا" إلى عنصر ما (تولًّد الحدثُ mouseover)، فعند مغادرته إيّاه سنحصل دائما على mouseout. Mouseout عند المغادرة نحو عنصر ابني من الميزات المهمّة للحدث mouseout هي أنّه يقع عندما يتحّرك المؤشّر من عنصرٍ ما إلى عناصره السليلة، كأن يتحرّك من #parent إلى #child في شيفرة HTML هذه: <div id="parent"> <div id="child">...</div> </div> فلو كنّا في #parent ثم حرّكنا المؤشّر إلى داخل #child، فسنتحصّل على mouseout في #parent! قد يبدو هذا غريبا، لكن يمكن تفسيره بسهولة. حسب منطق المتصفّح، يمكن للمؤشّر أن يكون فوق عنصر واحد فقط في نفس الوقت، وهو العنصر الذي يأتي في اﻷسفل وفق ترتيب القيمة z-index. فإذا ذهب المؤشّر نحو عنصر آخر (ولو كان سليلًا)، فإنّه يغادر العنصر السابق. يُرجى التنبّه إلى جرئيّة مهمّة أخرى في معالجة اﻷحداث. ينتشر الحدث mouseover الذي يقع في عنصر سليل نحو اﻷعلى. فإذا كان للعنصر #parent معالج للحدث mouseover فإنّه سيشتغل: يمكنك رؤية ذلك جيّدا في المثال الذي من هنا. العنصر <div id="child"> هو داخل العنصر <div id="parent">. هناك معالجات لـ mouseover/out مسندة إلى العنصر #parent تعمل على إخراج تفاصيل الحدث. فإذا حرّكتَ الفأرةَ من #parent نحو #child، ستلاحظ وقوع حدثين في #parent: mouseout [target: parent] (مغادرة المؤشّر للعنصر اﻷب)، ثم mouseover [target: child] (مجيء المؤشّر إلى العنصر الابن، انتشر هذا الحدث نحو اﻷعلى). See the Pen JS-p2-mousemove-mouseover-mouseout-mouseenter-mouseleave-ex03 by Hsoub (@Hsoub) on CodePen. كما هو ظاهر، عندما يتحرّك المؤشّر من العنصر #parent نحو العنصر #child، يشتغل معالجان في العنصر اﻷب: mouseout وmouseover: parent.onmouseout = function(event) { /* العنصر اﻷب :event.target */ }; parent.onmouseover = function(event) { /* العنصر الابن (انتشر نحو اﻷعلى) :event.target */ }; إذا لم نفحص event.target داخل المعالجات، فقد يبدو اﻷمر وكأنّ مؤشّر الفأرة قد غادر العنصر `#parent ثم عاد إليه مباشرة. لكن ليس اﻷمر كذلك! لا يزال المؤشر فوق العنصر اﻷب، وقد تحرّك فقط إلى داخل العنصر الابن. إذا كانت هناك أفعال تحصل بمغادرة العنصر اﻷبويّ مثل إجراء حركات في parent.onmouseout، فلا نودّ حصولها عادة بمجرّد انتقال المؤشّر في داخل #parent. لتجنّب ذلك، يمكننا تفحّص relatedTarget في المعالج، وإذا كان المؤشّر لا يزال داخل العنصر، فإنّنا نتجاهل الحدث. أو بدلًا عن ذلك، يمكننا استخدام اﻷحداث mouseenter وmouseleave التي سنتناولها الآن، إذ ليس لها مثل هذه المشاكل. اﻷحداث mouseenter وmouseleave الأحداث mouseenter/mouseleave مثل الأحداث mouseover/mouseout، فهي تقع عندما يدخل/يغادر مؤشّر الفأرة العنصر. لكن هناك فرقان مهمّان: لا تُحتسب الانتقالات التي تحدث داخل العنصر، من وإلى عناصره السليلة. لا تنتشر الأحداث mouseenter/mouseleave نحو الأعلى. هذه الأحداث بسيطة للغاية. عندما يدخل المؤشّر العنصر، يقع mouseenter، ولا يهمّ مكانه داخل العنصر أو داخل عناصره السليلة بالضبط. ثمّ عندما يغادر المؤشّر العنصر، يقع mouseleave. يشبه المثال الذي يمكن مشاهدته من هنا المثال أعلاه، لكن العنصر العلويّ الآن لديه mouseenter/mouseleave بدل mouseover/mouseout. مثلما يمكن أن ترى، الأحداث التي تولّدت هي الأحداث الناتجة عن تحريك المؤشّر إلى داخل وخارج العنصر العلويّ فقط. لا شيء يحدث عندما يذهب المؤشر إلى العنصر الابن ويرجع. تُتجاهل الانتقالات بين العناصر السليلة. تفويض الأحداث الأحداث mouseenter/leave بسيطة للغاية وسهلة الاستخدام. لكنّها لا تنتشر نحو الأعلى. بالتالي لا يمكننا استخدام تفويض الأحداث معها. تصوّر أنّنا نريد معالجة دخول/مغادرة الفأرة لخانات جدول ما. وهناك المئات من الخانات. بكون الحلّ الطبيعيّ عادةً بإسناد المعالج إلى <table> ومعالجة الأحداث هناك. لكنّ mouseenter/leave لا تنتشر نحو الأعلى. فإذا وقعت الأحداث في <td>، فلا يمكن التقاطها إلّا بواسطة معالج مُسنَد إلى ذلك العنصر <td>. تشتغل المعالجات المسندة إلى <table> فقط عندما يدخل/يغادر المؤشّر الجدول ككلّ. يستحيل الحصول على أيّة معلومات حول الانتقالات داخله. لذا، فلنستخدم mouseover/mouseout. لنبدأ بمعالجات بسيطة تعمل على إبراز العنصر الذي تحت المؤشّر. // لنبرز العنصر الذي تحت المؤشّر table.onmouseover = function(event) { let target = event.target; target.style.background = 'pink'; }; table.onmouseout = function(event) { let target = event.target; target.style.background = ''; }; يمكنك مشاهدتها تعمل من هنا. كلّما تنقّلت الفأرة عبر العناصر في هذا الجدول، سيكون العنصر الحالي بارزا على الدوام: See the Pen JS-p2-mousemove-mouseover-mouseout-mouseenter-mouseleave-ex04 by Hsoub (@Hsoub) on CodePen. في حالتنا هذه نودّ أن نعالج الانتقالات بين خانات الجدول <td>، أي عند دخول خانة أو مغادرتها. لا تهمّنا الانتقالات الأخرى كالتي تحصل داخل الخانة أو خارج كلّ الخانات. لنُنحّيها جانبا. هذا ما يمكننا فعله: حفظ العنصر <td> المُبرَز حاليّا في متغيّر، ولنسمّيه currentElem. تجاهل الحدث mouseover عند وقوعه في داخل العنصر <td> الحالي. هذا مثال لشيفرة تأخذ جميع الحالات الممكنة في الحسبان: // التي تحت الفأرة الآن (إن وُجدت) <td> let currentElem = null; table.onmouseover = function(event) { // قبل دخول عنصر جديد، تغادر الفأرة دائما العنصر السابق // السابق <td> نفسه، فإنّنا لم نغادر currentElem إذا بقي العنصر // بداخله، تجاهل الحدث mouseover هذا مجرّد if (currentElem) return; let target = event.target.closest('td'); // تجاهل - <td> تحرّكنا إلى غير if (!target) return; // لكن خارج جدولنا (يمكن ذلك في حالة الجداول المتداخلة) <td> تحرّكنا داخل // تجاهل if (!table.contains(target)) return; // جديد <td> مرحا! دخلنا currentElem = target; onEnter(currentElem); }; table.onmouseout = function(event) { // الآن، تجاهل الحدث <td> إذا كنّا خارج أيّ // <td> يُحتمل أنّ هذا تحرّك داخل الجدول، ولكن خارج // آخر <tr> إلى <tr> مثلا من if (!currentElem) return; // نحن بصدد مغادرة العنصر – إلى أين؟ ربمّا إلى عنصر سليل؟ let relatedTarget = event.relatedTarget; while (relatedTarget) { // currentElem اصعد مع سلسلة الآباء وتحقّق إذا كنّا لا نزال داخل // فهو إذًا انتقال داخليّ – تجاهله if (relatedTarget == currentElem) return; relatedTarget = relatedTarget.parentNode; } // .حقًّا .<td> لقد غادرنا العنصر onLeave(currentElem); currentElem = null; }; // أيّة دالّة لمعالجة دخول/مغادرة العنصر function onEnter(elem) { elem.style.background = 'pink'; // أظهر ذلك في المساحة النصيّة text.value += `over -> ${currentElem.tagName}.${currentElem.className}\n`; text.scrollTop = 1e6; } function onLeave(elem) { elem.style.background = ''; // أظهر ذلك في المساحة النصيّة text.value += `out <- ${elem.tagName}.${elem.className}\n`; text.scrollTop = 1e6; } مرّة أخرى، أهمّ ما يميّز هذه الطريقة هو التالي: تستخدم تفويض الأحداث لمعالجة دخول/مغادرة أيّ <td> داخل الجدول. فهي تعتمد على mouseover/out بدل mouseenter/leave التي لا تنتشر نحو الأعلى وبذلك لا تسمح بتفويض الأحداث. تُنحّى الأحداث الأخرى، مثل التنقّل بين العناصر السليلة لـ <td>، جانبًا لكيلا تشتغل المعالجات onEnter/Leave إلّا عندما يدخل أو يغادر المؤشّر <td> ككلّ. يمكنك مشاهدة المثال كاملا بجميع التفاصيل من هنا: See the Pen JS-p2-mousemove-mouseover-mouseout-mouseenter-mouseleave-ex05 by Hsoub (@Hsoub) on CodePen. جرّب تحريك المؤشّر إلى داخل وخارج خانات الجدول وكذلك بداخلها. بسرعة أو ببطء، لا يهمّ ذلك. لا تُبرَز إلّا <td> ككلّ، بخلاف المثال السابق. الملخص تناولنا الأحداث mouseover وmouseout وmousemove وmouseenter وmouseleave. من الجيّد التنبّه لهذه الأمور: قد تتخطّى الحركةُ السريعة للفأرة العناصر البينيّة. تملك الأحداث mouseover/out وmouseenter/leave الخاصيّة الإضافيّة relatedTarget، وهي تمثّل العنصر الذي أتينا منه/إليه، وتكمّل الخاصيّة target. تقع الأحداث mouseover/out حتى لو ذهبنا من عنصر أبويّ إلى عنصر ابنيّ. يَفترِض المتصفّحُ أنّه يمكن للفأرة أن تكون فوق عنصر واحد فقط في نفس الوقت، وهو العنصر الذي في الأسفل. تختلف الأحداث mouseenter/leave في هذا الجانب، فهي تقع فقط عندما تأتي الفأرة من/إلى العنصر ككلّ. إضافة إلى ذلك، هي لا تنتشر نحو الأعلى. التمارين سلوك التلميحة المحسَّن الأهميّة: 5 اكتب شيفرة جافاسكربت لإظهار تلميحة فوق العنصر الذي له السمة data-tooltip. يجب أن تصير قيمة هذه السمة نصّ التلميحة. يشبه هذا التمرين سلوك التلميحة، لكن يمكن هنا أن تكون العناصر الموسّمة متداخلة، ويحب حينها أن تُعرض التلميحة التي تكون في الأسفل. ويمكن أن تظهر تلميحة واحدة فقط في نفس الوقت. على سبيل المثال: <div data-tooltip="Here – is the house interior" id="house"> <div data-tooltip="Here – is the roof" id="roof"></div> ... <a href="https://en.wikipedia.org/wiki/The_Three_Little_Pigs" data-tooltip="Read on…">Hover over me</a> </div> يمكن مشاهدة النتيجة من هنا. أنجز التمرين في البيئة التجريبية الحل افتح الحل في البيئة التجريبية تلميحة "ذكيّة" الأهمية: 5 اكتب دالّة لإظهار تلميحة فوق عنصر ما فقط عندما يحرّك المستخدم الفأرة إلى داخل العنصر وليس عبره. بعبارة أخرى، إذا حرّك المستخدم الفأرة إلى عنصر ما وتوقّفت هناك، أظهر التلميحة. وإذا حرّكها مرورا به فقط، فلا داعي لذلك، إذ لا أحد يريد ومضات زائدة. فنيّا، يمكننا قياس سرعة الفأرة فوق العنصر، فإذا كانت بطيئة يمككنا افتراض أنّها أتت "فوق العنصر" وإظهار التلميحة. وإذا كانت سريعة تجاهلناها. أنشئ كائنا عموميّا new HoverIntent(options) لهذا الغرض، له الخيارات options التالية: elem هو العنصر الذي نتتبّعه. over هي الدالّة التي نستدعيها إذا أتت الفأرة إلى العنصر: أي إذا تحرّكت ببطء أو توقّفت فوقه. out هي الدالّة التي نستدعيها إذا غادرت الفأرة العنصر (في حال استُدعيت over). هذا مثال لاستخدام هذا الكائن للتلميحة: // تلميحة تجريبيّة let tooltip = document.createElement('div'); tooltip.className = "tooltip"; tooltip.innerHTML = "Tooltip"; // over/out سيتتبّع الكائن الفأرة ويستدعي new HoverIntent({ elem, over() { tooltip.style.left = elem.getBoundingClientRect().left + 'px'; tooltip.style.top = elem.getBoundingClientRect().bottom + 5 + 'px'; document.body.append(tooltip); }, out() { tooltip.remove(); } }); يمكن مشاهدة النتيجة من هنا. إذا حرّكت الفأرة فوق "الساعة" بسرعة فلن يحصل شيء، وإذا حرّكتها ببطء أو توقّفت عندها، فستكون هناك تلميحة. يُرجى التنبّه: لا "تومض" التلميحة عندما يتحرّك المؤشّر بين العناصر الفرعيّة للساعة. أنجز التمرين في البيئة التجريبية. الحل تبدو الخوارزمية بسيطة: أسند معالجات onmouseover/out إلى العنصر. يمكن أيضا استخدام onmouseenter/leave هنا، لكنّها أقلّ عموما، ولن تعمل إذا أدخلنا التفويض. إذا دخل المؤشّر العنصر، ابدأ بقياس السرعة في mousemove. إذا كانت السرعة صغيرة، شغّل over. عند الذهاب إلى خارج العنصر وقد نُفّذت over، شغّل out. لكن كيف تُقاس السرعة؟ قد تكون أوّل فكرة: شغّل دالّة كلّ 100ms وقس المسافة بين الإحداثيات السابقة والجديدة. إذا كانت المسافة صغيرة، فإنّ السرعة صغيرة. للأسف، لا سبيل للحصول على"الإحداثيّات الحاليّة للفأرة" في جافاسكربت. لا وجود لدالّة مثل getCurrentMouseCoordinates(). السبيل الوحيد للحصول على الإحداثيّات هو الإنصات لأحداث الفأرة، مثل mousemove وأخذ الإحداثيّات من كائن الحدث. لنضع إذًا معالجًا للحدث mousemove، لتتبّع الإحداثيّات وتذكّرها. ثمّ لمقارنتها مرّة كلّ 100ms. ملاحظة: تَستخدم اختباراتُ الحلّ dispatchEvent لرؤية ما إذا كانت التلميحة تعمل جيّدا. شاهد الحل في البيئة التجريبية. ترجمة -وبتصرف- للمقال Moving the mouse: mouseover/out, mouseenter/leave من سلسلة Browser: Document, Events, Interfaces لصاحبها Ilya Kantor1 نقطة
-
مقبس عميل TCP إنشاء مقبس يستخدم TCP (Transmission Control Protocol) $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); تأكّد من أنّ المقبس (socket) أُنشئ بنجاح، تُستخدم الدالة onSocketFailure لمعالجة أخطاء المقبس، مثال: if(!is_resource($socket)) onSocketFailure("Failed to create socket"); اتصال المقبس بعنوان محدد يفشل السطر الثاني بأمان إذا فشل الاتصال: socket_connect($socket, "chat.stackoverflow.com", 6667) or onSocketFailure("Failed to connect to chat.stackoverflow.com:6667", $socket); إرسال بيانات إلى الخادم ترسل الدالة socket_write البايتات عبر مقبس، تُمثَّل مصفوفة البايت في PHP بسلسلة نصية والتي هي غير حساسة للترميز بشكلٍ طبيعي. socket_write($socket, "NICK Alice\r\nUSER alice 0 * :Alice\r\n"); إرسال بيانات من الخادم تستقبل الشيفرة التالية بعض البيانات من الخادم باستخدام الدالة socket_read، إنّ تمرير قيمة المعامل الثالث على أنّها PHP_NORMAL_READ يقرأ حتى البايت \r/\n ويُضمَّن في القيمة المُعادة، أما تمريرها على أنّها PHP_BINARY_READ يقرأ الكمية المطلوبة من بيانات المجرى. تُعيد الدالة socket_read القيمة false مباشرةً إذا اُستدعيت الدالة socket_set_nonblock قبلها واُستخدمَت القيمة PHP_BINARY_READ، وإلا يُعطَّل التابع حتى تُستقبَل بيانات كافية (الوصول إلى الطول المحدد في المعامل الثاني أو الوصول إلى نهاية السطر) أو يُغلَق المقبس. يقرأ المثال التالي بيانات من خادم IRC (Internet Relay Chat) بشكلٍ افتراضي: while(true) { // قراءة سطر من المقبس $line = socket_read($socket, 1024, PHP_NORMAL_READ); if(substr($line, -1) === "\r") { // (1) socket_read($socket, 1, PHP_BINARY_READ); } $message = parseLine($line); if($message->type === "QUIT") break; } يُقرأ في الموضع (1) بايت واحد من المقبس أو يتم تجاوزه، نفرض أنّ البايت التالي في المجرى يجب أن يكون \n ويعدّ هذا ممارسة سيئة ويجعل السكربت ضعيفًا ومعرّضًا لقيم غير متوقعة. إغلاق المقبس يؤدي إغلاق المقبس إلى تحريره وتحرير الموارد المرتبطة به. socket_close($socket); مقبس خادم TCP إنشاء المقبس إنشاء مقبس يستخدم TCP نفس طريقة إنشاء مقبس عميل. $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); ربط المقبس يكون اتصال الربط من شبكة ما (المعامل 2) إلى منفذ معين للمقبس (المعامل 3)، المعامل الثاني عادةً هو "0.0.0.0" والذي يقبل الاتصال من كل الشبكات. أحد أشهر الأسباب التي تؤدي إلى حدوث خطأ نتيجة التابع socket_bind هو أن يكون العنوان المحدد مقيد مسبقًا بعملية أخرى. تُنهى عادةً العمليات الأخرى يدويًا (لمنع إنهاء العمليات الحرجة دون قصد) لذا تُحرَّر المقابس. socket_bind($socket, "0.0.0.0", 6667) or onSocketFailure("Failed to bind to 0.0.0.0:6667"); ضبط المقبس للاستماع نستخدم التابع socket_listen لجعل المقبس يستمع إلى الاتصالات الواردة، يعبر المعامل الثاني عن عدد الاتصالات الأعظمي المسموح لها أن تكون في قائمة الانتظار قبل قبولها. socket_listen($socket, 5); معالجة الاتصال خادم TCP هو خادم يعالج الاتصالات الأبناء، يُنشئ التابع socket_accept اتصال ابن جديد. $conn = socket_accept($socket); نقل بيانات الاتصال من التابع socket_accept هو نفسه في مقبس عميل TCP. يمكنك استدعاء التابع socket_close($conn); مباشرةً عندما تريد إغلاق الاتصال، ولا يؤثر هذا على مقبس خادم TCP الأصلي. إغلاق الخادم يجب استدعاء التابع socket_close($socket); عند الانتهاء من استخدام الخادم، مما سيؤدي إلى تحرير عنوان TCP وبالتالي يُسمح لعمليات أخرى بالربط معه. مقبس خادم UDP خادم UDP (user datagram protocol) لا يعتمد على المجرى بل على الرزمة (packet-based) على عكس TCP، مثلًا عميل يرسل بيانات في وحدات تدعى رزم (packets) إلى الخادم ويعرف العميل العملاء من خلال عناوينهم، لا يوجد دالة مضمَّنة تربط الرزم المختلفة المُرسلة من نفس العميل (على عكس TCP حيث تُعالج البيانات المُرسلة من نفس العميل بمورد محدد ينشأه التابع socket_accept)، يمكننا التفكير أنّه عند كل وصول لرزمة UDP فإنّ اتصال TCP جديد يُقبَل ويُغلق. إنشاء مقبس خادم UDP $socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); ربط المقبس إلى عنوان نفس المعاملات الموجودة لخادم TCP. socket_bind($socket, "0.0.0.0", 9000) or onSocketFailure("Failed to bind to 0.0.0.0:9000",$socket); إرسال رزمة ترسل الشيفرة التالية المتغير $data في رزمة UDP إلى $address:$port. socket_sendto($socket, $data, strlen($data), 0, $address, $port); استقبال رزمة تحاول الشيفرة التالية إدارة رزم UDP بطريقة تعتمد على فهرسة العميل. $clients = []; while (true){ socket_recvfrom($socket, $buffer, 32768, 0, $ip, $port) === true or onSocketFailure("Failed to receive packet", $socket); $address = "$ip:$port"; if (!isset($clients[$address])) $clients[$address] = new Client(); $clients[$address]->handlePacket($buffer); } إغلاق الخادم يمكن استخدام التابع socket_close على مورد مقبس خادم UDP. سيحرر هذا عنوان UDP مما يسمح بربط العمليات الأخرى إلى هذا العنوان. معالجة أخطاء المقبس نستخدم التابع socket_last_error للحصول على رقم معرِّف الخطأ الأخير من إضافة المقابس، ونستخدم التابع socket_strerror لتحويل هذا الرقم إلى سلسلة نصيّة قابلة للقراءة من قِبل الإنسان. function onSocketFailure(string $message, $socket = null) { if(is_resource($socket)) { $message .= ": " . socket_strerror(socket_last_error($socket)); } die($message); } مقابس الويب (Webscockets) ينفّذ استخدام إضافة المقبس (socket) واجهة منخفضة المستوى لدوال اتصال المقبس بالاعتماد على مقابس BSD (Berkeley Software Distribution) الشائعة، مما يوفر إمكانية العمل كخادم مقبس وعميل. خادم TCP/IP بسيط يمكنك أن تجد هنا مثالًا بسيطًا يعتمد على توثيق PHP الرسمي. أنشئ سكربت مقبس ويب يستمع إلى المنفذ 5000 باستخدام putty والطرفية لتنفيذ الأمر telnet 127.0.0.1 5000 (المضيف المحلي)، يرد هذا السكربت بالرسالة التي أرسلتها (كتعقب عكسي): <?php // تعطيل المهلة set_time_limit(0); // تعطيل التخزين المؤقت للخرج ob_implicit_flush(); // الإعدادات $address = '127.0.0.1'; $port = 5000; // (1) if (($socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)) === false) { echo "Couldn't create socket".socket_strerror(socket_last_error())."\n"; } // (2) if (socket_bind($socket, $address, $port) === false) { echo "Bind Error ".socket_strerror(socket_last_error($sock)) ."\n"; } if (socket_listen($socket, 5) === false) { echo "Listen Failed ".socket_strerror(socket_last_error($socket)) . "\n"; } do { if (($msgsock = socket_accept($socket)) === false) { echo "Error: socket_accept: " . socket_strerror(socket_last_error($socket)) . "\n"; break; } /* إرسال رسالة ترحيب */ $msg = "\nPHP Websocket \n"; // الاستماع إلى دخل المستخدم do { if (false === ($buf = socket_read($msgsock, 2048, PHP_NORMAL_READ))) { echo "socket read error: ".socket_strerror(socket_last_error($msgsock)) . "\n"; break 2; } if (!$buf = trim($buf)) { continue; } // الرد على المستخدم برسالته $talkback = "PHP: You said '$buf'.\n"; socket_write($msgsock, $talkback, strlen($talkback)); // طباعة الرسالة على الطرفية echo "$buf\n"; } while (true); socket_close($msgsock); } while (true); socket_close($socket); ?> في الموضع (1) لدينا الدالة socket_create لها الشكل العام ( int $domain , int $type , int $protocol ): يمكن أن يكون المتغير $domain هو AF_INET أو AF_INET6 من أجل IPV6 أو AF_UNIX من أجل بروتوكول الاتصال المحلي. يمكن أن يكون المتغير $protocol إما SOL_TCP أو SOL_UDP (TCP/UDP) تعيد هذه الدالة القيمة true في حالة النجاح. في الموضع (2) نستخدم الدالة socket_bind التي لها الشكل العام socket_bind ( resource $socket , string $address [, int $port = 0 ] )، تربط هذه الدالة المقبس ليستمع إلى عنوان ومنفذ محددين. استيثاق HTTP سنكتب سكربت استيثاق ترويسة HTTP بسيط، ولاحظ أنّه يجب وضع هذه الشيفرة في ترويسة الصفحة وإلا لن يعمل: <?php if (!isset($_SERVER['PHP_AUTH_USER'])) { header('WWW-Authenticate: Basic realm="My Realm"'); header('HTTP/1.0 401 Unauthorized'); echo 'Text to send if user hits Cancel button'; exit; } echo "<p>Hello {$_SERVER['PHP_AUTH_USER']}.</p>"; // حفظ المعلومات $user = $_SERVER['PHP_AUTH_USER']; echo "<p>You entered {$_SERVER['PHP_AUTH_PW']} as your password.</p>"; // حفظ كلمة المرور (يمكن إضافة التشفير اختياريًا) $pass = $_SERVER['PHP_AUTH_PW']; //Save the password(optionally add encryption)! ?> // صفحة html ترجمة -وبتصرف- للفصول [ WebSockets - HTTP Authentication - Sockets] من كتاب PHP Notes for Professionals book اقرأ أيضًا المقال التالي: إضافة PHP MySQLi ونظام إدارة قواعد البيانات SQLite3 المقال السابق: معالجة بيانات طلبيات HTTP والتعامل مع أخطاء رفع الملفات في PHP1 نقطة
-
واجهة mysqli هي تحسين (وتعني إضافة MySQL محسَّنة "MySQL Improvement extension") لواجهة MySQL التي أُهملت في الإصدار 5.5 وحُذِفت في الإصدار 7.0. طُوِّرت إضافة mysqli المعروفة أيضًا باسم إضافة MySQL المحسَّنة للاستفادة من إيجابيات الميزات الجديدة الموجودة في إصدارات أنظمة MySQL بدءًا من الإصدار 4.1.3 وما بعد. ضُمِّنت إضافة mysqli في الإصدار PHP 5 وما بعده. إغلاق الاتصال يُنصَح بإغلاق الاتصال بعد الانتهاء من الاستعلام من قاعدة البيانات لتحرير الموارد. النمط الكائني التوجه $conn->close(); النمط الإجرائي mysqli_close($conn); ملاحظة: سيُغلق الاتصال مع الخادم حالما ينتهي تنفيذ السكربت ما لم يُغلق مبكرًا باستدعاء دالة إغلاق الاتصال بشكلٍ صريح. حالة استخدام: يجب بكل تأكيد إغلاق الاتصال إذا احتوى السكربت على كمية معقولة من المعالجة بعد جلب النتيجة واستعاد مجموعة النتائج كاملةً، إذا لم نقم بذلك فهناك احتمال أن يصل خادم MySQL إلى حد الاتصال عندما يكون خادم الويب قيد الاستخدام المكثف. اتصال MySQLi النمط الكائني التوجه الاتصال بالخادم: $conn = new mysqli("localhost","my_user","my_password"); ضبط قاعدة البيانات الافتراضية: $conn->select_db("my_db"); الاتصال بقاعدة البيانات: $conn = new mysqli("localhost","my_user","my_password","my_db"); النمط الإجرائي الاتصال بالخادم: $conn = mysqli_connect("localhost","my_user","my_password"); ضبط قاعدة البيانات الافتراضية: mysqli_select_db($conn, "my_db"); الاتصال بقاعدة البيانات: $conn = mysqli_connect("localhost","my_user","my_password","my_db"); التحقق من الاتصال بقاعدة البيانات النمط الكائني التوجه if ($conn->connect_errno > 0) { trigger_error($db->connect_error); } // else: successfully connected النمط الإجرائي if (!$conn) { trigger_error(mysqli_connect_error()); } // else: successfully connected تمرير حلقة على نتائج MySQLi تجعل PHP من السهل الحصول على البيانات من النتائج وتكرارها باستخدام تعليمة while، وترجع false عندما تفشل في الحصول على السطر التالي وتنتهي الحلقة. تعمل هذه الأمثلة مع: mysqli_fetch_assoc - مصفوفة ترابطية مع أسماء الأعمدة كمفاتيح. mysqli_fetch_object - كائن stdClass مع أسماء الأعمدة كمتغيرات. mysqli_fetch_array - مصفوفة ترابطية وعددية (يمكنك استخدام وسيط لتحصل على إحداها) mysqli_fetch_row - مصفوفة عددية. النمط الكائني التوجه while($row = $result->fetch_assoc()) { var_dump($row); } النمط الإجرائي while($row = mysqli_fetch_assoc($result)) { var_dump($row); } يمكننا استخدام الشيفرة التالية للحصول على معلومات دقيقة من النتائج: while ($row = $result->fetch_assoc()) { echo 'Name and surname: '.$row['name'].' '.$row['surname'].'<br>'; // طباعة معلومات من العمود 'age' echo 'Age: '.$row['age'].'<br>'; } تعليمات التحضير في MySQLi يمكنك قراءة المزيد حول منع حقن SQL مع الاستعلامات التي تحوي معاملات لمعرفة سبب مساعدة تعليمات التحضير لك في تأمين تعليمات SQL ضد هجمات حقن SQL. المتغير $conn هو كائن MySQLi في الشيفرات التالية وفي المثالين نفرض أنّ $sql: $sql = "SELECT column_1 FROM table WHERE column_2 = ? AND column_3 > ?"; تمثّل ? القيم التي سنوفرها لاحقًا، لاحظ أننا لا نحتاج علامات الاقتباس للنصوص البديلة بغض النظر عن النوع، يمكننا أيضًا أن نوفر النصوص البديلة فقط في جزء البيانات من الاستعلام أي SET وVALUES وWHERE، ولا يمكن استخدام النصوص البديلة في الأجزاء SELECT أو FROM. النمط الكائني التوجه if ($stmt = $conn->prepare($sql)) { $stmt->bind_param("si", $column_2_value, $column_3_value); $stmt->execute(); $stmt->bind_result($column_1); $stmt->fetch(); // يمكننا الآن استخدام المتغير $column_1 مثل أي متغير PHP آخر $stmt->close(); } النمط الإجرائي if ($stmt = mysqli_prepare($conn, $sql)) { mysqli_stmt_bind_param($stmt, "si", $column_2_value, $column_3_value); mysqli_stmt_execute($stmt); // جلب البيانات هنا mysqli_stmt_close($stmt); } يُحدَّد المعامل الأول للتابع $stmt->bind_param أو المعامل الثاني للتابع mysqli_stmt_bind_param وفقًا لنمط البيانات للمعامل المقابل في استعلام SQL: table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } المعامل نوع البيانات للمعامل المقيّد i integer عدد صحيح d double عدد عشري s string سلسلة نصية b blob كائن بيانات ثنائية يجب أن تكون قائمة المعاملات بنفس الترتيب الموجود في الاستعلام، تعني si في هذا المثال أنّ المعامل الأول (column_2 = ?) هو سلسلة نصية والمعامل الثاني (column_3 > ?) عدد صحيح. سلاسل الهروب النصية سلاسل الهروب النصية (Escaping Strings) طريقة قديمة (وأقل أمنًا) لتأمين البيانات لإدراجها في استعلام، تعمل باستخدام دالة MySQL mysql_real_escape_string() لمعالجة وتعقيم البيانات (أي أنّ PHP لا تقوم بعملية الهروب). توفر واجهة برمجة التطبيقات MySQLi الوصول المباشر لهذه الدالة. $escaped = $conn->real_escape_string($_GET['var']); //أو $escaped = mysqli_real_escape_string($conn, $_GET['var']); عند هذه النقطة يصبح لديك سلسلة نصية تعدها MySQL آمنة للاستخدام في استعلام مباشر. $sql = 'SELECT * FROM users WHERE username = "' . $escaped . '"'; $result = $conn->query($sql); إذًا لم لا تُعدّ هذه الطريقة آمنة مثل تعليمات التحضير؟ لأنه يوجد عدة طرق لخداع MySQL لإنتاج سلسلة نصية تُعد آمنة. إليك المثال التالي: $id = mysqli_real_escape_string("1 OR 1=1"); $sql = 'SELECT * FROM table WHERE id = ' . $id; لا يمثّل التعبير 1 OR 1=1 بيانات ستهرّبها MySQL، لكنه لا يزال يمثّل حقن SQL، يوجد أمثلة أخرى تمثّل حالات تُعاد فيها بيانات غير آمنة، المشكلة هي أنّ دالة الهروب في MySQL صُممت لجعل البيانات تتوافق مع صيغة SQL وليس للتأكد من أنّ MySQL لا تتمكن من خلط بيانات المستخدم من أجل تعليمات SQL. تنقيح أخطاء SQL في MySQLi الاستعلام في الشيفرة التالية سيفشل (استخدمنا المتغير $conn للاتصال بالخادم كما وضحنا سابقًا): $result = $conn->query('SELECT * FROM non_existent_table'); نتيجة $result هي false وهذا لا يساعدنا على معرفة الخطأ، لحسن الحظ يمكن لمتغير الاتصال $conn أن يخبرنا عن الفشل: trigger_error($conn->error); أو بالنمط الإجرائي: trigger_error(mysqli_error($conn)); عندها ستحصل على خطأ مشابه لما يلي: Table 'my_db.non_existent_table' doesn't exist استعلام MySQLi تأخذ الدالة query سلسلة SQL صحيحة وتنفّذها مباشرةً على الاتصال بقاعدة البيانات $conn. النمط الكائني التوجه $result = $conn->query("SELECT * FROM `people`"); النمط الإجرائي $result = mysqli_query($conn, "SELECT * FROM `people`"); سيظهر تحذير، المشكلة الشائعة هنا هي أنّك تنفّذ الشيفرة ببساطة وتتوقعها أن تعمل (تعيد مثلًا كائنًا من الصنف mysqli_stmt)، بما أنّ هذه الدالة تأخذ سلسلة نصية فقط فأنت تبني الاستعلام أولًا وإذا وُجدَت أي أخطاء في SQL سيفشل مصرِّف MySQL وعندها تعيد الدالة القيمة false. // سيفشل الاستعلام التالي $result = $conn->query('SELECT * FROM non_existent_table'); $row = $result->fetch_assoc(); ستنتج الشيفرة السابقة خطأ E_FATAL لأنّ نتيجة $result هي false وليس كائنًا. PHP Fatal error: Call to a member function fetch_assoc() on a non-object الخطأ الإجرائي مشابه لكنه ليس خطأً فادحًا لأننا نخترق توقعات الدالة فقط. // نفس الاستعلام السابق $row = mysqli_fetch_assoc($result); ستحصل على الرسالة التالية من PHP: mysqli_fetch_array() expects parameter 1 to be mysqli_result, boolean given يمكنك تجنب هذا باستخدام شيفرة الاختبار التالية: if($result) $row = mysqli_fetch_assoc($result); كيفية الحصول على البيانات من تعليمات التحضير تعليمات التحضير اطّلع على تعليمات التحضير في mysqli لمعرفة كيفية تحضير وتنفيذ استعلام. ربط النتائج النمط الكائني التوجه $stmt->bind_result($forename); النمط الإجرائي mysqli_stmt_bind_result($stmt, $forename); مشكلة استخدام الدالة bind_result أنّها تتطلب تعليمة لتحديد الأعمدة التي ستُستخدم، أي أنّه كي تعمل الشيفرة السابقة يجب أن يبدو الاستعلام بالشكل SELECT forename FROM users، لتضمين المزيد من الأعمدة يمكنك إضافتها كمعاملات للدالة bind_result (وتأكد من إضافتهم إلى استعلام SQL). نُسند في كلتا الحالتين العمود forename للمتغير $forename، تأخذ هذه الدالة عدد وسائط بعدد الأعمدة التي تريد إسنادها، يتم هذا الإسناد مرة واحدة بما أنّ الدالة تُربَط بالمرجع، لذا يمكننا تمرير حلقة بالشكل التالي: النمط الكائني التوجه while ($stmt->fetch()) echo "$forename<br />"; النمط الإجرائي while (mysqli_stmt_fetch($stmt)) echo "$forename<br />"; العيب في هذا أنّك تحتاج لإسناد الكثير من المتغيرات في وقت واحد وهذا يجعل تتبع الاستعلامات الكبيرة أمرًا صعبًا، إذا كان لديك محرك MySQL أساسي (mysqlnd) مثبّت فإنّ كل ما تحتاجه هو استخدام get_result. النمط الكائني التوجه $result = $stmt->get_result(); النمط الإجرائي $result = mysqli_stmt_get_result($stmt); يعدّ هذا سهلًا لأننا نحصل على كائن من الصنف mysqli_result? وهو نفس الكائن الذي تعيده mysqli_query أي أنّه يمكنك استخدام حلقة لعرض النتيجة. ماذا لو لم نستطع تثبيت mysqlnd؟ يمكنك الاطلاع على جواب مناسب من هنا. يمكن أن تؤدي هذه الدالة مهمة get_result بدون أن تكون مثبّتة على الخادم، فهي ببساطة تكرر النتائج وتبني مصفوفة ترابطية. function get_result(\mysqli_stmt $statement) { $result = array(); $statement->store_result(); for ($i = 0; $i < $statement->num_rows; $i++) { $metadata = $statement->result_metadata(); $params = array(); while ($field = $metadata->fetch_field()) { $params[] = &$result[$i][$field->name]; } call_user_func_array(array($statement, 'bind_result'), $params); $statement->fetch(); } return $result; } يمكننا بعدها استخدام الدالة للحصول على النتائج كما لو أننا نستخدم mysqli_fetch_assoc(): <?php $query = $mysqli->prepare("SELECT * FROM users WHERE forename LIKE ?"); $condition = "J%"; $query->bind_param("s", $condition); $query->execute(); $result = get_result($query); while ($row = array_shift($result)) { echo $row["id"] . ' - ' . $row["forename"] . ' ' . $row["surname"] . '<br>'; } سيظهر لنا نفس الخرج في حالة استخدام محرك mysqlnd باستثناء أنّه لا يحتاج للتثبيت، وهذا الحل مناسب جداً إذا كنت لا تستطيع تثبيت المحرك. إضافة معرف في MySQLi استعادة آخر معرّف مولَّد من استعلام INSERT على جدول فيه عمود AUTO_INCREMENT. النمط الكائني التوجه $id = $conn->insert_id; النمط الإجرائي $id = mysqli_insert_id($conn); ترجع الشيفرة السابقة القيمة صفر إذا لم يكن هناك استعلام سابق على الاتصال أو إذا لم يحدّث الاستعلام قيمة AUTO_INCREMENT. إضافة معرّف عند تحديث الأسطر لا تُرجع تعليمة UPDATE معرّف السطر المُضاف في الحالة العادية، إذ يُرجَع المعرّف AUTO_INCREMENT عند حفظ سطر جديد فقط (أو إضافته)، يمكن استخدام الصياغة INSERT ... ON DUPLICATE KEY UPDATE للتحديث مما يجعل التحديثات تطرأ على المعرِّف الجديد. مثال: CREATE TABLE iodku ( id INT AUTO_INCREMENT NOT NULL, name VARCHAR(99) NOT NULL, misc INT NOT NULL, PRIMARY KEY(id), UNIQUE(name) ) ENGINE=InnoDB; INSERT INTO iodku (name, misc) VALUES ('Leslie', 123), ('Sally', 456); Query OK, 2 rows affected (0.00 sec) Records: 2 Duplicates: 0 Warnings: 0 id name misc 1 Leslie 123 2 Sally 456 في حال حدّثَ الجدول IODKU واستعاد LAST_INSERT_ID() المعرِّف المرتبط: $sql = "INSERT INTO iodku (name, misc) VALUES ('Sally', 3333) ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id), misc = VALUES(misc)"; $conn->query($sql); $id = $conn->insert_id; يجب أن تحدّث الشيفرة السابقة السطر الثاني من الجدول وتعيد القيمة الحالية للمعرِّف (2). الحالة التي يحدث فيها إضافة سطر وتعيد LAST_INSERT_ID() المعرِّف الجديد: $sql = "INSERT INTO iodku (name, misc) VALUES ('Dana', 789) ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id), misc = VALUES(misc); $conn->query($sql); $id = $conn->insert_id; يجب أن تضيف الشيفرة السابقة سطرًا جديدًا إلى الجدول وتعيد القيمة الأخيرة للمعرِّف (3). ينتج عن الاستعلام: SELECT * FROM iodku; الجدول التالي: id name misc 1 Leslie 123 2 Sally 3333 3 Dana 789 درس سريع لمكتبة SQLite3 إليك مثال كامل عن جميع واجهات برمجة التطبيقات الشائعة الاستخدام المرتبطة بمكتبة SQLite3، بهدف جعلك تعمل بسرعة كبيرة ويمكنك أيضاً الحصول على ملف PHP قابل للتنفيذ لهذا الدرس. إنشاء/فتح قاعدة بيانات ننشئ قاعدة بيانات أولاً، ننشئها فقط إذا لم يكن الملف موجوداً ونفتحها للقراءة/الكتابة، امتداد الملف يعود لك لكن الشائع هو استخدام الامتداد .sqlite الأكثر إيضاحًا. $db = new SQLite3('analytics.sqlite', SQLITE3_OPEN_CREATE | SQLITE3_OPEN_READWRITE); إنشاء جدول $db->query('CREATE TABLE IF NOT EXISTS "visits" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "user_id" INTEGER, "url" VARCHAR, "time" DATETIME )'); إضافة عينة بيانات يُنصح بإحاطة الاستعلامات المرتبطة ضمن عملية (transaction) (مع الكلمات المفتاحية BEGIN وCOMMIT) حتى لو لم تكن مهتمًا بالترابط، إذا لم تفعل ذلك فإنّ SQLite ستحيط كل استعلام مفرد بعملية بشكلٍ تلقائي مما يؤدي إلى البطء الشديد. قد تُفاجئ بسبب بطء تعليمات INSERT إذا كنت جديدًا على استخدام SQLite. $db->exec('BEGIN'); $db->query('INSERT INTO "visits" ("user_id", "url", "time") VALUES (42, "/test", "2017-01-14 10:11:23")'); $db->query('INSERT INTO "visits" ("user_id", "url", "time") VALUES (42, "/test2", "2017-01-14 10:11:44")'); $db->exec('COMMIT'); يمكنك القيام بإضافة بيانات قد لا تكون آمنة مع تعليمة تحضير، باستخدام المعاملات ذات الأسماء (named parameters): $statement = $db->prepare('INSERT INTO "visits" ("user_id", "url", "time") VALUES (:uid, :url, :time)'); $statement->bindValue(':uid', 1337); $statement->bindValue(':url', '/test'); $statement->bindValue(':time', date('Y-m-d H:i:s')); $statement->execute(); you can reuse the statement with different values جلب البيانات لنجلب زيارات اليوم للمستخدم رقم 42، سنستخدم تعليمة التحضير من جديد لكن هذه المرة مع معاملات ذات أسماء وهذا أكثر إيجازًا: $statement = $db->prepare('SELECT * FROM "visits" WHERE "user_id" = ? AND "time" >= ?'); $statement->bindValue(1, 42); $statement->bindValue(2, '2017-01-14'); $result = $statement->execute(); echo "Get the 1st row as an associative array:\n"; print_r($result->fetchArray(SQLITE3_ASSOC)); echo "\n"; echo "Get the next row as a numeric array:\n"; print_r($result->fetchArray(SQLITE3_NUM)); echo "\n"; ملاحظة: تُرجع الدالة fetchArray() القيمة false إذا لم يكن هناك المزيد من الأسطر، يمكنك الاستفادة من هذا في حلقة while. حرّر الذاكرة - لا يحدث هذا تلقائيًا أثناء تشغيل السكربت. $result->finalize(); الاختزالات إليك اختزال مفيد لجلب سطر واحد كمصفوفة ترابطية، يعني المعامل الثاني أننا نريد كل الأعمدة المُختارة. انتبه إلى أنّ هذا الاختزال لا يدعم ربط المعاملات لكن يمكنك بدلًا من ذلك الهروب من السلاسل النصية، ضع القيم دائمًا بين علامات اقتباس مفردة إذ تستخدم علامات الاقتباس المزدوجة لأسماء الجداول والأعمدة (بشكل مشابه لعلامات الاقتباس المائلة في MySQL). $query = 'SELECT * FROM "visits" WHERE "url" = \'' . SQLite3::escapeString('/test') . '\' ORDER BY "id" DESC LIMIT 1'; $lastVisit = $db->querySingle($query, true); echo "Last visit of '/test':\n"; print_r($lastVisit); echo "\n"; اختزال آخر مفيد لاستعادة قيمة واحدة فقط. $userCount = $db->querySingle('SELECT COUNT(DISTINCT "user_id") FROM "visits"'); echo "User count: $userCount\n"; echo "\n"; التنظيف في النهاية، أغلق قاعدة البيانات على الرغم من أنّ هذا يتم تلقائيًا عندما ينتهي السكربت. $db->close(); الاستعلام من قاعدة بيانات <?php // إنشاء كائن SQLite3 جديد من ملف قاعدة البيانات على الخادم $database = new SQLite3('mysqlitedb.db'); // الاستعلام من قاعدة البيانات باستخدام SQL $results = $database->query('SELECT bar FROM foo'); // التكرار على كل النتائج وإظهارهم على الصفحة while ($row = $results->fetchArray()) { var_dump($row); } ?> استعادة نتيجة واحدة فقط بالإضافة إلى استخدام تعليمات LIMIT في SQL، يمكنك استخدام الدالة querySingle في SQLite3 لاستعادة سطر واحد أو العمود الأول. <?php $database = new SQLite3('mysqlitedb.db'); //(1) $database->querySingle('SELECT column1Name FROM table WHERE column2Name=1'); // (2) $database->querySingle('SELECT column1Name, column2Name FROM user WHERE column3Name=1', true); في الموضع (1) بدون ضبط المعامل الثاني الاختياري للقيمة true سيُرجع الاستعلام العمود الأول من السطر الأول للنتائج فقط ويكون من نفس نوع columnName. في الموضع (2) مع المعامل الاختياري entire_row سيرجع هذا الاستعلام مصفوفة من كامل السطر الأول من نتائج الاستعلام. ترجمة -وبتصرف- للفصول من كتاب PHP Notes for Professionals book اقرأ أيضًا المقال التالي: استخدام MongoDB و Redis في PHP المقال السابق: المقابس (sockets) في PHP1 نقطة
-
1 نقطة
-
1 نقطة
-
كيف أستطيع تشغيل المشروع بلارافيل في حال كنت سأقوم بوضع الموقع على استضافة مشتركة؟ وبما أنه ليس لدي وصول إلى خادم الويب ولا أستطيع تشغيل الأوامر اللازمة. لقد حاولت تغيير المسار ليقوم بالقراءة من داخل ملف public وحاولت تعديل ملف .htaccess بالشكل التالي: <IfModule mod_rewrite.c> RewriteEngine On RewriteRule ^(.*)$ public/$1 [L] </IfModule> ولكن إما لا يظهر الموقع كلياً، أو أحصل على الخطأ التالي: NotFoundHttpException in compiled.php line 76101 نقطة
-
كيف يُمكن إنشاء متغير داخل ملف Blade جربت التالي: {{ $old_section = "whatever" }} لكن هذا يقوم بطباعة whatever أنا أعلم أنه يُمكن كتابة: <?php $old_section = "whatever"; ?>1 نقطة
-
في الإصدارات من 5.5 فما فوق يُمكن إستخدام التوجيه php و تعريف المتغيرات فيه أو إجراء تعلميمات php بداخله بهذا الشكل: @php $i = 1 @endphp او: @php ($i = 1) أما في الإصدارات الأقدم يُمكنك عمل التالي: {{-- */$i=0;/* --}} أو بإستخدام توجيه if بالشكل التالي: @if ($variable = 'any data') @endif ثم عند الإستخدام: {{ $variable }}1 نقطة
-
يمكن تعريف متغير في Blade PHP Block بالطريقة التالية: @php $hello = "Hello World!"; @endphp وهي تشبه الطريقة التي تقوم PHP بتعريف المتغيرات فيها أي الحالة العادية General PHP Block لكن يختلف شكل الوسم وللوصل للمتغير يمكن من خلال: {{$hello}} طرق أخرى .. @set($var, 10) @set($var2, 'some string') @section('someSection') {{ $yourVar = 'Your value' }} @endsection @php($old_section = "whatever")1 نقطة
-
أرغب في الحصول على قيمة من خلال SQL التالي باستخدام Eloquent ORM: SELECT COUNT(*) FROM (SELECT * FROM abc GROUP BY col1) AS a; فكرت في الطريقة التالية: $sql = Abc::from('abc AS a')->groupBy('col1')->toSql(); $num = Abc::from(\DB::raw($sql))->count(); print $num; ماهي الطريقة الصحيحة لفعل ذلك؟1 نقطة
-
في الإصدارات الحديثة من 5.5 فما فوق أصبح بالإمكان عمل التالي: Abc::selectSub(function($q) { $q->select('*')->groupBy('col1'); }, 'a')->count('a.*'); أو: Abc::selectSub(Abc::select('*')->groupBy('col1'), 'a')->count('a.*');1 نقطة
-
يمكننا عمل جدول فرعي يحوي الاستعلام المطلوب ثم نقوم بالاستعلام منه عن طريق الدالة mergeBindings أي يصبح الاستعلام بالشكل التالي: $count = DB::table( DB::raw("({$sub->toSql()}) as sub") ) ->mergeBindings($sub->getQuery()) // يمكنك هنا تحديد الاستعلام ->count(); ولكن بطريقة اسهل من إصدار Laravel v5.6.12 أضيفت دالتين ()fromSub() and fromRaw لباني الاستعلامات حيث أصبح بالامكان جلب البيانات من الاستعلامات الجزئية مباشرة: DB::query()->fromSub(function ($query) { $query->from('abc')->groupBy('col1'); }, 'a')->count(); | Abc::selectSub(Abc::select('*')->groupBy('col1'), 'a')->count('a.*'); حيث أصبح هذه الطريقة الأفضل.1 نقطة
-
إن السبب في ذلك هو بعض التغييرات التي طرأت على النسخة الثامنة في لارافيل. ففي النسخ السابقة من لارافيل، مزود خدمة المسارات RouteServiceProvider موضوع ضمن $namespace بشكل افتراضي، أما في نسخة لارافيل 8 فقيمتها الافتراضية هي null لكي تتيح للمبرمج من تعريف الاسماء الخاصة به. لذلك يجب عليك استخدام المسار الكامل ضمن التعريف عند عدم استخدام namespace محدد، كالتالي: use App\Http\Controllers\UserController; Route::get('/users', [UserController::class, 'index']); // أو Route::get('/users', 'App\Http\Controllers\UserController@index'); أما في حال كنت ترغب باعتماد الطريقة القديمة (الموجودة في الإصدارات السابقة من لارافيل) يجب عليك تعديل الملف App\Providers\RouteServiceProvider وإضافة namespace ضمن التابع boot كالتالي: public function boot() { Route::prefix('api') ->middleware('api') ->namespace('App\Http\Controllers') // <--------- نضيفها هنا ->group(base_path('routes/api.php')); } ولكن في حال كنت تستخدم نسخة أحدث من لارافيل 8.0.2 فأتاحت لك سهولة تنفيذ السابق عن طريق تعليق السطر التالي فقط: // protected $namespace = 'App\\Http\\Controllers'; وبذلك تعود لنفس الاستخدام كما هو في الإصدارات الأقدم من الإصدار رقم 8.1 نقطة
-
يمكن تغيير اسم الملف server.php والموجود في جذر المشروع إلى الاسم index.php ثم نسخ الملف htaccess من داخل مجلد public أيضاً إلى المجلد الرئيسي (الجذر) للمشروع. ولكن هذه الطريقة ليست الأفضل كونها تسمح ببعض الخروقات الأمنية، لأنها تسمح للمخترقين بالوصول إلى معلومات حساسة مثل المعلومات الموجودة ضمن الملف .env ولها أيضاً بعض العيوب الأمنية الأخرى. بدلاً من ذلك، يمكن إنشاء ملف جديد htaccess ضمن المجلد الرئيسي للمشروع أي بجانب الملف server.php ووضع بداخله التالي: RewriteEngine On RewriteCond %{REQUEST_URI} !^/public/ RewriteRule ^(.*)$ /public/$1 [L,QSA] والذي بدوره سيقوم بإعادة كتابة جميع URIs إلى المجلّد public حتى الترويسات التي يتم استقبالها على سبيل المثال authorization headers والمتغيرات الأخرى.1 نقطة
-
هل هناك لغة تجمع بين تطوير مواقع الويب و برامج الجوالات ؟ وماهي اللغة الأنسب لها ؟ ومن الأسهل في التعلم من بين هذه اللغات ( جافا سكربت , بايثون , سي شارب ) ؟1 نقطة
-
ربما يعود سبب هذه المشكلة لعدم حذف ملفات التخزين المؤقت cache عند رفع المشروع على الاستضافة فلا يتم التعرف على المسارات بشكل صحيح ولحل المشكلة يجب تنفيذ التعليمات التالية: php artisan composer dump-autoload php artisan config:cache php artisan config:clear php artisan cache:clear php artisan route:clear ___ php artisan optimize:clear ___ php artisan clear-compiled php artisan optimize php artisan cache:clear حسب نسخة لارافيل وفي حال عدم وجود ملف المكتبات vendor يحب تنفيذ: php artisan install ولكن ربما تكون مشكلة صلاحيات بسبب تغير الحاسوب/المستخدم فنقوم بالتالي: sudo chown -R www-data storage sudo chgrp -R www-data storage sudo chmod -R 775 storage1 نقطة
-
يختلف حل هذه المشكلة تبعاً لاختلاف نظام التشغيل أو البيئة التي يتم الاتصال عبرها وتنفيذ الأوامر، ولكن من الحلول المقترحة. فقد تظهر نتيجة عدم التمكن من تحديد الاتصال ضمن ملف .env وذلك بسبب تحديد بيئة مغايرة أثناء التطوير عن البيئة الحالية وبذلك يمكن تنفيذ الأمر التالي: php artisan migrate --env=production وبذلك سيتم أخذ قيم المتغيرات بشكل صحيح من ملف .env في حال كانت للنشر production. من إحدى الحلول أيضاً هو بتغيير قيمة DB_HOST من localhost إلى 127.0.0.1 والسبب في ذلك هو أن localhost يستخدم UNIX socket، أما 127.0.0.1 يستخدم TCP وهو Transmission Control Protocol والذي يجري عبر الانترنت. في حال لم تجدِ الطرق السابقة نفعاً، يمكن أن يكون ذلك بسبب اعتماد نسخة قديمة من mysql، ولذلك يجب التأكد من الإعدادات الموجودة عن طريق الدخول إلى خادم قواعد البيانات mysql و إجراء الأمر التالي: show variables like '%sock%' ثم أخذ القيمة التالية التي ستظهر نتيجة تنفيذ الأمر: /tmp/mysql.sock ثم التوجه إلى ملف database.php في مشروع لارافيل ووضع القيمة ضمن اتصال mysql كالتالي: 'mysql' => array( 'driver' => 'mysql', 'host' => 'localhost', 'database' => 'SchoolBoard', 'username' => 'root', 'password' => 'venturaa', 'charset' => 'utf8', 'collation' => 'utf8_unicode_ci', 'prefix' => '', 'unix_socket' => '/tmp/mysql.sock', ),1 نقطة
-
تشير رسالة الخطأ إلى أنه تمت تجربة اتصال MySQL عبر Socket (وهو أمر غير مدعوم). يمكنك استخدام بيئة مختلفة و تنفيذ الأمر التالي php artisan migrate --env=production يمكنك في Laravel 5 من تغيير قيمة DB_HOST في ملف .env من localhost إلى 127.0.0.11 نقطة
-
هل يمكن ان يتعلم الشخص اللغة بايثون بدون الانجليزية1 نقطة
-
لتعلم أي لغة برمجة ليس عليك أن تكون متفوق في اللغة الانجليزية, ولكن يكفي أن تعرف المصطلحات التي تستخدم في علم البرمجة, ولكن هذا لا يعني الابتعاد تماما عن تعلم اللغة الانجليزية, حيث أن تعلمها يساعد كثيرا في توسيع سوق العمل الخاص بك ويزيد من فرص حصولك على الوظائف والمشاريع في مواقع العمل الحر فمن خلالها تستطيع مخاطبة العالم بأكمله وليس دول محددة فقط, تستطيع تعلمها بالتوازي مع تعلمك لغة بايثون.1 نقطة
-
الإنجليزية ليست شرطاً من شروط تعلم اي لغة من لغات البرمجة حيث أن الاكواد التي تراها ليست عبارة عن لغة إنجليزية وإنما هي مجموعة من الكلمات المحجوزة و التي هي بمثابة المصطلحات التي يتعين عليك معرفتها و معرفة طريقة كتابتها بصورة صحيحة ولكن مع ذلك فإن تعلم اللغة الإنجليزية سيكون مفيداً لك للإطلاع على اوراق العمل التي ينشر غالبها باللغة الإنجليزية او قراءة كتب البرمجة التي تكتب باللغة الإنجليزية فهي مجرد عامل مساعد وليست شرطاً لتعلم لغات البرمجة.1 نقطة
-
إن كل اتصال مع قاعدة بيانات في لارافيل يلزمه الخصائص التالية ضمن ملف .env: DB_CONNECTION=mysql نوع قاعدة البيانات DB_HOST=127.0.0.1 المضيف إما محلي أو خادم خارجي DB_PORT=3306 المنفذ DB_DATABASE=database1 اسم قاعدة البيانات DB_USERNAME=root اسم المتسخدم DB_PASSWORD=pass كلمة المرور ولذلك يمكن تكرارها عند كل تعريف لقاعدة بيانات. أما بالنسبة لاستخدام الاتصال فبالإضافة لما تم شرحه في التعليق السابق، يمكنك عن طريق Eloquent المقدّمة من لارافيل تعريف الاتصال، فمثلاً في حال وجود model ترغب باستخدامه دائماً مع اتصال محدد أو قاعدة بيانات محددة، فلا داعي لتكرار ذكر عملية الاتصال في كل مرة ستقوم باستخدامه، يكفي وضع $connection في Eloquent ضمن المودل الخاص بك وذلك سيضمن إبقاء الاتصال نفسه، كالتالي: class SomeModel extends Eloquent { protected $connection = 'mysql2'; }1 نقطة
-
1 نقطة
-
يمكنك ذلك من خلال وضع الأكواد التالية في ملف .env DB_CONNECTION_SECOND=mysql DB_HOST_SECOND=127.0.0.1 DB_PORT_SECOND=3306 DB_DATABASE_SECOND=database2 DB_USERNAME_SECOND=root DB_PASSWORD_SECOND= فلاحظ جيدا اننا قمنا بتعريف متغيرات لقاعدة بيانات أخرى, ثم في مسار config/database.php نقوم بوضع 'mysql2' => [ 'driver' => env('DB_CONNECTION_SECOND'), 'host' => env('DB_HOST_SECOND'), 'port' => env('DB_PORT_SECOND'), 'database' => env('DB_DATABASE_SECOND'), 'username' => env('DB_USERNAME_SECOND'), 'password' => env('DB_PASSWORD_SECOND'), ], تحت mysql بحيث يكون هذا يكون معلومات الاتصال بقاعدة البيانات الأخرى. يمكنك صنع جدول لقاعدة البيانات عبر Schema بحيث نعرف Schema::connection لقاعدة البيانات الأخرى مثل Schema::connection('mysql2')->create('users_two', function($table) { $table->increments('id'): }); يمكنك استخدام Query Builder بهذا الشكل $posts = DB::connection('mysql2')->select(); ايضا يمكنك توجيه Model إلى استخدام قاعدة الاتصال الثاني الخاص بقاعدة البيانات الثانية باستخدام protected $connection = 'mysql2'; ايضا يمكنك في Controller استخدام الشكل التالي : public function index() { $post = new Post; $post->setConnection('mysql2'); $something = $post->find(1); $get_posts = Post::on('mysql2')->find(1); return $get_posts; }1 نقطة
-
هنا يجب استخدام inner join كي تستطيع جلب أخر رسالة لكل عضو فمثلا: SELECT column_name(s) FROM table1 INNER JOIN table2 ON table1.column_name = table2.column_name; طبعا inner join مقصود بها الربط الداخلي تستخدم لربط جدولين أو اكثر وذلك لتحديد بعض السجلات من الجداول المترابطة مع بعضها, ويمكن استخدامها بعدة صيغ حسب ما تريد الاستعلام عنه, ففي حالتك فلنفترض ان جدول الأعضاء اسمه users وجدول الرسائل اسمه messages فتكون إلية الربط بينهم باستخدام inner join هكذا. SELECT m.msg, u.name FROM messages m INNER JOIN users u ON m.recipient_id = u.id فلاحظ أننا استخدمنا اسماء مستعارة للجداول فجدول users قمنا بإعطائه الاسم المستعار u و جدول messages قمنا بإعطائه الاسم المستعار m و قمنا في الاستعلام السابق بجلب نص الرسالة مع اسم العضو الذي قام بارسالها وقمنا بالتعويض عن اسم الجدول باسمه المستعار.1 نقطة
-
في هذا الكود ابسط طريقة وهي إذا لا تريد أحداث تغييرات في الكود يمكنك إضافة limit. <?php $stmt = $db->prepare('SELECT * FROM messages WHERE recipient_id = ? Order By id desc'); $stmt->bind_param('i', $_SESSION['id']); $stmt->execute(); $result = $stmt->get_result(); while ($row = $result->fetch_assoc()) { echo '<div class="div_messages"><a href="index.php?to='.$row['sender_id'].'"> تم استلام رسالة من <br />'.$row['sender_name'].'<br />'.$row['date'].'</a></div>' ; } $stmt->close(); ?> يصبح <?php $stmt = $db->prepare('SELECT * FROM messages WHERE recipient_id = ? Order By id desc limit 1'); $stmt->bind_param('i', $_SESSION['id']); $stmt->execute(); $result = $stmt->get_result(); while ($row = $result->fetch_assoc()) { echo '<div class="div_messages"><a href="index.php?to='.$row['sender_id'].'"> تم استلام رسالة من <br />'.$row['sender_name'].'<br />'.$row['date'].'</a></div>' ; } $stmt->close(); ?> في الكود الثاني ممكن تغيير الشرط بحيث يصبح OR و ليس And . ولكن يتم التحديد ومساعدتك بشكل أفضل إذا أرفقت مجلد المشروع كاملا. هنا يتم استخدام ajax لجلب البيانات بدون تحديث الصفحة. بحيث تقوم بكتابة كود PHP يرجع قيمة عدد الاشعارات بصيغة json و من ثم استخدام ajax لجلب القيمة بدون تحديث للصفحة.1 نقطة
-
Attempted import error: 'initReactI18next' is not exported from 'react-i18next' هذا الخطأ يخبرك بصراحة أن initReactI18next لا يمكن استرادها من react-i18next وهذا قد يكون ناتج عن عدة أسباب منها أن المكتبة ليست مثبتة في المشروع أو أن المكتبة حدث خطأ ما عند تثبيتها و للتأكد من ذلك قم بحذف ال node_modules و package.lock.json أعد تثبيت كل المكتبات . السبب الأخر قد يكون أنهم أجرو تغيير في المكتبة وغيروا كيفية استيراد initReactI18next من react-i18next ولهذا داءما يجب التحقق من التوثيق الحديث والرسمي للمكتبة لمواكبة التغييرات. وأيضا أحيانا عند تثبيت مكتبات في مشروع react يجب ايقاف المشروع واعادة تشغيله فذلك قد يحل هذه المشكلة.1 نقطة
-
مرحباً @عبد الواحد الحدادي إذا كنت تستخدم البوتستراب يمكنك عملها هكذا المهم أن تخرج العناصر خارج العنصر <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"></div> لأنه المسؤل عن إخفاء العناصر في المقاسات الصغيرة1 نقطة
-
يمكن إرفاق الكود الخاص بك لمساعدتك في وضع الروابط, أو يمكنك بعد وسم <a class="navbar-brand" href="#">Navbar</a> وضع بعده الرابطين حسب ما تريد مثل <a class="navbar-brand" href="#">اطلب عرض سعر</a> <a class="navbar-brand" href="#">اطلب تطبيق فكرة</a> و من ثم في باستخدام css يمكنك التحكم في محاذاة و مكان الروابط. جميع ما بداخل كلاس collapse navbar-collapse يتم إخفاءه و يتم إظهاره في قائمة المنسدلة على شاشات الجوال عند الضغط على ايقونة البار.1 نقطة
-
ربما سبب المشكلة بعدم تطابق كلمة السر يعود إلى عدم تشفيرها باستخدام الدالة bcrypt عند إنشاء الحساب و قبل إضافة الكلمة المشفرة لقاعدة البيانات في الدالة Create الخاصة بالنموذج. RegisterController.php الخاص بالمستخدم وفرضا هو User وتجنب تطبيق التشفير / التجزئة على كلمة المرور أكثر من مرة لأنه يعطي كلمة جديدة ولن يحدث مطابقة عند عمل الاختبار checkPassword1 نقطة
-
تستخدم الدالة lockAsync لعمل lock على الشاشة بحيث لا يستطيع المستخدم دوران الفيديو. على الجانب الآخر، تستخدم unlockAsync لفك هذا القفل. في الكود المرفق، إذا أردت أن ترجع الشاشة مرة أخرى إلى الوضعية الإفتراضية، عليك استخدام lockAsync ايضاً، ولكن مع اعطائها الوضعية الجديدة (Portrait على سبيل المثال). if (fullscreenUpdate === Video.FULLSCREEN_UPDATE_PLAYER_DID_PRESENT) { await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE); } else { // لاحظ استخدام lockAsync مرة اخرى ولكن مع الوضعية الجديدة await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP); } return true;1 نقطة
-
أحاول إستخدام Laravel Sail عند إنشاء مشروع لارافيل قمت بتنفيذ الأمر: curl -s https://laravel.build/sail-test | bash ثم نفذت الأمر: cd sail-test && ./vendor/bin/sail up كل شيء تم بشكل صحيح و عند تجربة المشروع من خلال http://localhost. يعمل بشكل جيد لكن عند تنفيذ أمر التهجير: sail artisan migrate:install يظهر الخطأ: Illuminate\Database\QueryException SQLSTATE[HY000] [2002] No route to host (SQL: create table `migrations` (`id` int unsigned not null auto_increment primary key, `migration` varchar(255) not null, `batch` int not null) default character set utf8mb4 collate 'utf8mb4_unicode_ci') at vendor/laravel/framework/src/Illuminate/Database/Connection.php:678 674▕ // If an exception occurs when attempting to run a query, we'll format the error 675▕ // message to include the bindings with SQL, which will make this exception a 676▕ // lot more helpful to the developer instead of just the database's errors. 677▕ catch (Exception $e) { ➜ 678▕ throw new QueryException( 679▕ $query, $this->prepareBindings($bindings), $e 680▕ ); 681▕ } 682▕ +29 vendor frames 30 artisan:37 Illuminate\Foundation\Console\Kernel::handle() كيف يُمكن حل المُشكلة؟1 نقطة
-
ربما يعود الخطأ لوجود كلمة سر في ملف env. حيث يجب وضع القيمة التالية في ملف docker-compose.yml ليتم قراءة كلمة السر.. MYSQL_ALLOW_EMPTY_PASSWORD 'no' ثم للتأكد من ربط الحاوية بشكل صحيح مع قاعدة البيانات mysql container IP يجب تنفيذ التعليمة التالية: $ docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' <container_name_or_container_ID> حيث تعيد ip ثم نضيفها في env file. DB_HOST=<ip_address> ip_address المعاد من التعليمة السابقة أو DB_HOST=mysql ثم نكمل العمل : $ sail artisan migrate في حال لم تعمل يجب تسجيل الدخول login in sail إلى الحاوية عن طريق: sail shell ثم نكمل artisan migrate.1 نقطة
-
يجب التوجه أولاً إلى مجلد المشروع الرئيسي وتنفيذ الأمر: sail shell بعدها سيتم تسجيل الدخول إلى المحفظة الخاصة بالمشروع ويمكنك تنفيذ أمر التهجير: sail artisan migrate يمكنك أيضاً استخدام اسم المحفظة نفسه الموجود في docker والذي تستطيع إيجاده ضمن: docker-compose.yml والذي يكون بشكل افتراضي: DB_HOST=mysql1 نقطة
-
في الـ Generator Functions، أي expression على يمين yield يتم حسابه ثم يتم تسليم النتيجة للمتصل. في السطر الثاني، سيقوم التأثير all بحساب حالة الـ mode والـ theme، ولكن يجب استخدام yield قبلها هكذا: export function* appOnLoad() { const { mode, theme } = yield all({ ... }); } يمكنك التفكير في التأثيرات كتعليمات للـ middleware لإجراء بعض العمليات (مثلاً، عمل dispatch asynchronous).1 نقطة
-
تجدر بنا الإشارة إلى قاعدة مهمة قبل الشروع في الحديث عن كيفية تعامل جافاسكربت مع الأنماط والأصناف. القاعدة بديهية جدًا ولكننا سنذكرها للإفادة. هناك طريقتان تُستخدمان لتنسيق عنصرٍ ما: إنشاء صنف في ملف CSS وإضافته للعنصر على الشكل التالي: <div class="..."> كتابة خاصيات السمة style مباشرة بين تسلسلات التهريب الخاصة بالعنصر على الشكل التالي: <div style="..."> تستطيع لغة جافاسكربت التعديل على خاصيات الأصناف وخاصيات الخاصية style. ويُستحسن استعمال أصناف CSS للتنسيق بدلا من السمة style، حيث نلجأ إلى الطريقة الثانية فقط إذا تعذّرعلينا إضافة التنسيق باستعمال الطريقة الأولى. فعلى سبيل المثال، يمكن استعمال الخاصية style إذا كان عليك حساب إحداثيات عنصر ما ديناميكيًا وتحديد قيمها باستعمال جافاسكربت كالآتي: let top = /* عمليات حسابية معقّدة */; let left = /* عمليات حسابية معقّدة */; elem.style.left = left; // e.g '123px', تُحسب أثناء التنفيذ elem.style.top = top; // e.g '456px' وفي حالات أخرى مثل تلوين النص بالأحمر، أو إضافة صورة للخلفية، يُستحسن وصف التنسيق باستعمال CSS ثم إضافة الصنف للعنصر (يمكن عمل ذلك باستعمال جافاسكربت)، حيث يمنحك ذلك أكثر مرونة وسهولة في البرمجة. اسم الصنف (className) وقائمة الأصناف (classList) يُعدّ تعديل الصنف أكثر عمليةٍ نُصادفها في السكربتات. وكانت جافاسكربت فيما مضى تتّسمُ بالمحدودية حين يتعلّق الأمر بالكلمة المحجوزة "class"، حيث لم تكن تسمح بأن تحمل خاصيةٌ من خواص الكائن (object) اسم "class" كالآتي : elem.class. حينها جاء التفكير في استحداث خاصية مشابهة تسمى "className" تُطبّق على الأصناف. حيث يمثِّل elem.className اسم السمة "class" كما هو مبين في المثال التالي: <body class="main page"> <script> alert(document.body.className); // main page </script> </body> See the Pen JS-p2-08-Styles and classes-ex1 by Hsoub (@Hsoub) on CodePen. في حالة ما إذا أسندنا قيمةً معينةً للخاصية elem.className، تعوِّض هذه القيمة مُجمل سلسلة الأصناف. هذا ما نحتاج إليه أحيانا، ولكننا نحتاج في غالبية الحالات إلى إضافة أو حذف صنفٍ واحدٍ فقط. ولهذا وُجدت خاصية أخرى؛ إنها خاصية elem.classList. تُعدّ هذه الخاصية كائنًا خاصًا بحد ذاته له دوالّه الخاصة (methods) لإضافة صنف ما (add) أو حذفه (remove) أو إما إضافته إن لم يكن موجودًا أو حذفه إن وُجد (toggle).كما هو مبين في المثال التالي: <body class="main page"> <script> // إضافة صنف document.body.classList.add('article'); alert(document.body.className); // main page article </script> </body> See the Pen JS-p2-08-Styles and classes-ex2 by Hsoub (@Hsoub) on CodePen. وبذلك يمكننا إجراء عمليات على مجمل سلسلة الأصناف دفعة واحدة باستعمال className أو على الأصناف، كلٌّ على حدى، باستعمال classList. اختيارنا لهذا أو ذاك مرتبط بما نحتاج القيام به. الدوالّ الخاصة بـالخاصية classList هي: *elem.classList.add/remove("class"): إضافة الصنف المذكور كوسيط للدلّة/حذف الصنف المذكور كوسيط للدلّة. *elem.classList.toggle("classe"): إضافة الصنف المذكور كوسيط للدلّة إن لم يكن موجودًا أو حذفه إن وُجد. *elem.classList.contains("class"): البحث عن الصنف المذكور كوسيط للدلّة، والنتيجة تكون صحيح أو خطأ (true/false). وتقبل الخاصية classList الإدماج داخل حلقة التكرار for....of لإظهار قائمة الأصناف كما في المثال التالي: <body class="main page"> <script> for (let name of document.body.classList) { alert(name); // main, and then page } </script> </body> See the Pen JS-p2-08-Styles and classes-ex3 by Hsoub (@Hsoub) on CodePen. تنسيق العنصر باستعمال الخاصية style تُعدّ الخاصية elem.style كائنًا يحمل محتوى السمة style. ويؤدي إسناد القيمة "100px" للخاصية elem.style.width على الشكل التالي: elem.style.width="100px" إلى النتيجة نفسها لو كانت السمة style تحمل السلسلة النصية "width:100px". إذا كان اسم السمة يتكون من عدة كلمات، يُشكَّل اسم الخاصية بجعل الحرف الأول من كل كلمة حرفا كبيرًا ماعدا الكلمة الأولى كما في المثال التالي: background-color => elem.style.backgroundColor z-index => elem.style.zIndex border-left-width => elem.style.borderLeftWidth ملاحظة: الخاصيات التي تبدأ ببادئة تتبع الخاصيات التي تبدأ ببادئة تُحدّد المتصفح نفس القاعدة، كالخاصيتين -moz-border-radius و-webkit-border-radius، حيث تُترجَم الشرطة إلى حرفٍ كبيرٍ كالآتي: button.style.MozBorderRadius = '5px'; button.style.WebkitBorderRadius = '5px'; تغيير قيمة خاصية التنسيق style يحدث أن تَرغب في إسناد قيمةٍ للخاصية style ثم حذفها لاحقا. يمكننا على سبيل المثال إسناد القيمة "none" للخاصية elem.style.display كالأتي elem.style.display = "none" ثم حذفها وكأننا لم نحدّد لها قيمةً من قبل. هنا، ينبغي إسناد سلسلة نصية فارغة للخاصية elem.style.display كالآتي elem.style.display = "" بدلا من حذفها (delete). //عند تنفيذ هذا السكربت يختفي العنصر <body> ثمّ يُعاود الظهور document.body.style.display = "none"; // يختفي setTimeout(() => document.body.style.display = "", 1000); // يُعاود الظهور إذا أسندنا سلسلة نصية فارغة للخاصية style.display، يُطبِّق المتصفح أصناف CSS والأنماط التنسيقية المتضمَّنة بداخلها بطريقة عاديةٍ جدًا وكأن الخاصية style.display غير موجودة تماما. ملاحظة: التعديل على خاصيات الخاصية style جملةً واحدةً باستعمال الخاصية style.cssStyle تُستعمل عادة الخاصية *.style للتعديل على قيم خاصيات التنسيق، كلٌ على حدى، ولا يمكننا التعديل عليها دفعة واحدة كالآتي: div.style="color:red; width:100px"، لأن div.style هو كائنٌ لا يمكن التعديل عليه (ِread-only) بهذه الطريقة. يمكن تغيير التنسيق كاملا دفعة واحدة بإسناد سلسلة نصية (تحمل وصف التنسيق) للخاصية style.cssStyle كما في المثال التالي: <div id="div">Button</div> <script> // يمكننا استعمال رايات تنسيقية خاصة مثل الراية “important” div.style.cssText=`color: red !important; background-color: yellow; width: 100px; text-align: center; `; alert(div.style.cssText); </script> See the Pen JS-p2-08-Styles and classes-ex4 by Hsoub (@Hsoub) on CodePen. غير أنه من النادر استعمال هذه الخاصية كونها تحذف الأنماط التنسيقية السابقة وتستبدلها بالقيم الجديدة، أي أنها قد تحذف أشياء مازلنا بحاجتها. فيما يمكن أن تُستخدم لتنسيق العناصر الجديدة، فلن يؤدي إسناد القيم بهذه الطريقة إلى أيّ عملية حذف (بما أن العناصر الجديدة لا تملك تنسيقات بعد). ويمكننا عمل ذلك أيضا باستعمال الدالّة div.setAttribute('style', 'color: red...'). الوحدات لا تنس إضافة الوحدات للقيم في شيفرة CSS، فلا يصِحُّ إسناد القيمة "10" للخاصية elem.style.top بل القيمة 10px هي الأصح، وإلا فلن يعمل السكربت بالشكل المطلوب. <body> <script> // لا يعمل document.body.style.margin = 20; alert(document.body.style.margin); // '' (سلسلة نصية فارغة، إهمال عملية الإسناد) // يعمل بعد إضافة الوحدة document.body.style.margin = '20px'; alert(document.body.style.margin); // 20px alert(document.body.style.marginTop); // 20px alert(document.body.style.marginLeft); // 20px </script> </body> See the Pen JS-p2-08-Styles and classes-ex5 by Hsoub (@Hsoub) on CodePen. لاحظ في السطرين الأخيرين أن المتصفح يفكّك الخاصية style.margin إلى خاصيتين وهما: style.marginLeft وstyle.marginTop. الأنماط المحسوبة باستعمال الدالة getComputedStyle يُعدّ التعديل على الأنماط عمليةً سهلةً ولكن كيف تُقرأ الأنماط؟ نريد مثلا معرفة مقاس، هوامش ولون عنصرٍ ما، كيف نتحصل عليها؟ تعمل الخاصية style على تعديل قيمة السمة style فقط دون الوصول إلى الأنماط الموصوفة في الأوراق التنسيقية المتتالية CSS. وبالتالي لا يمكننا قراءة أيّ قيمٍ من أصناف CSS باستعمال الخاصية elem.style. فعلى سبيل المثال لا يمكن أن تصل الخاصية style في هذا المثال إلى الهامش. <head> <style> body { color: red; margin: 5px } </style> </head> <body> The red text <script> alert(document.body.style.color); // فارغة alert(document.body.style.marginTop); // فارغة </script> </body> See the Pen JS-p2-08-Styles and classes-ex6 by Hsoub (@Hsoub) on CodePen. ماذا لو أردنا على سبيل المثال إضافة 20px للهامش؟ سيكون علينا أولا الوصول إلى القيمة الحالية له حتى يتسنى لنا تعديلها. وهنا لدينا طريقة أخرى للحصول على ذلك وتكون باستعمال الدالة getComputedStyle وبنيتها كالآتي: getComputedStyle(element, [pseudo]) حيث يمثّل العنصر element العنصر الذي سنحسب قيمه ويمثل pseudo العنصر الزائف، مثلا before::. إذا كانت قيمة pseudo عبارة عن سلسلة نصية فارغة أو غير موجودة أصلا فهذا يعني أننا نقصد العنصر نفسه. وتكون مخرجات الدالّة (output) عبارة عن كائن يحوي أنماط تنسيقية مثله مثل elem.style ولكن يأخذ في الحسبان هذه المرة كلّ الأصناف الموجودة في ملف CSS. وفيما يلي مثال على ذلك: <head> <style> body { color: red; margin: 5px } </style> </head> <body> <script> let computedStyle = getComputedStyle(document.body); // يمكننا الآن قراءة اللون والهامش alert( computedStyle.marginTop ); // 5px alert( computedStyle.color ); // rgb(255, 0, 0) </script> </body> See the Pen JS-p2-08-Styles and classes-ex7 by Hsoub (@Hsoub) on CodePen. ملاحظة: القيم المحسوبة والقيم النهائية (المُحدَّدة) هناك مفهومان في لغة CSS هما: قيمة تنسيقية محسوبة وهي القيمة المتحصّل عليها بعد تطبيق مجمل القواعد التنسيقية وقواعد الوراثة المُتضمَّنة في ملف CSS. قد تكون على شكل height:1em أو font-size:125% . قيمة تنسيقية نهائية (مُحدَّدة) وهي القيمة التي يقع عليها الاختيار في آخر المطاف وتُطبَّق على العنصر. القيمتان 1em و125% هما قيمتان نسبيتان. يأخذ المتصفح كافة القيم المحسوبة ويجعل كافة الوحدات مطلقة كما في المثال التالي: height:20px ،font-size:16px. ويمكن للقيم النهائية الخاصة بالخاصيات الهندسية أن تكون عشرية مثل: width:50.5px. لقد اُستحدثت الدالّة getComputedStyle أساسا للحصول على قيم محسوبة ولكن تبيّن فيما بعد أن القيم النهائية (المُحدَّدة) أحسن، فتغيرت المعايير، وأصبحت الدالّة getComputedStyle تُخرِج القيم النهائية للخاصية والتي تكون غالبا بالبكسل px بالنسبة للخاصيات الهندسية. ملاحظة: تتطلب الدالة getComputedStyle ذكر الاسم الكامل للخاصية علينا البحث على الدوام عن الخاصية التي نحتاج إليها بدقة مثل: padingLeft، أو marginTop أو borderTopWidth وإلا فلن نتمكن من ضمان صحة النتيجة المتحصّل عليها. فمع وُجود، على سبيل المثال، الخاصيتين padingLeft/padingTop، على ماذا سوف نحصل عند تنفيذ الدالّة getComputedStyle(elem, pading)؟ لن نحصل على شيئ؟ أو ربما سنحصل على قيمة "مستوحاة" من قيم معرّفة مسبقا للحاشية (pading)؟ في الحقيقة لا يوجد أيّ معايير تتحدث عن هذا الموضوع. وهناك بعض التناقضات الأخرى، حيث تُظهِر بعض المتصفحات (Chrome مثلا) في مثال الموالي القيمة 10px، ولا تُظهِرها متصفحات أخرى (كالمتصفح Firefox). مثال: <style> body { margin: 10px; } </style> <script> let style = getComputedStyle(document.body); alert(style.margin); // نحصل على سلسلة فارغة عند استعمال المتصفح Firefox </script> See the Pen JS-p2-08-Styles and classes-ex8 by Hsoub (@Hsoub) on CodePen. ملاحظة: الأنماط التي تُطبَّق على الروابط :visited تكون مخفية يمكن تلوين الروابط التي سبق وأن زيرت باستخدام الصنف الزائف :visited في ملف CSS. لكن الدالّة getComputedStyle لا يمكنها الوصول إلى هذا اللون، لأن ذلك يمكِّن أيّ صفحة كانت من إنشاء الرابط على الصفحة والإطلاع على الأنماط وبالتالي معرفة ما إذا كان المستخدم قد زار الرابط من قبل. لا يمكن للغة جافاسكربت الإطلاع على الأنماط المعرّفة باستخدام الصنف الزائف :visited، كما تمنع لغة CSS تطبيق تنسيقاتِ تغيير الشكل والأبعاد (geometry-changing styles) ضمن الصنف الزائف :visited وذلك لغلق الطريق أمام أيّ صفحةٍ مشبوهةٍ تسعى لمعرفة ما إذا زار المستخدم الرابط أم لا، وبالتالي التعدي على خصوصيته. الخلاصة هناك خاصيتان تُستخدمان للعمل على الأصناف وهما: className: وهي سلسلة نصية تُستخدم للعمل على كافة الأصناف دفعةً واحدةً. classList: هي عبارة عن كائن له دوالّه الخاصة (add/delete/toggle/contains) وتستخدم للعمل على الأصناف، كلُ على حدى. ولتغيير التنسيق لدينا: الخاصية style؛ وهي عبارة عن كائن تُشكَّل خواصه بجعل الحرف الأول من كل كلمة حرفا كبيرًا ما عدا الكلمة الأولى. تُعدّ قراءته والتعديل عليه تماما كالتعديل على خاصيات السمة style، كلٌ على حدى. وللاطلاع على كيفية إضافة الراية important وغيرها، يمكنك زيارة موقع MDN حيث تجد قائمة من الدوالّ التي تُستخدم لذلك. الخاصية style.cssText: هي الخاصية التي تقابِل السمة "style" في مجملها، أي السلسلة النصية التي تحمل كافة الأنماط التنسيقية دفعةً واحدةً. ولقراءة الأنماط التنسيقية النهائية (التي تأخذ في الحسبان كافة الأصناف بعد تطبيق تنسيقات CSS وحساب القيم النهائية) وُجدَت الدالة getComputedStyle(elem, [pseudo]) والتي تخرِج/تعيد كائنا يحمل التنسيقات وهو قابلٌ للقراءة فقط. تمرين إنشاء إشعار درجة الأهمية: 5 اكتب شيفرة الدالّة showNotification(options) التي تُنشِئ إشعارًا كالآتي: بالمحتوى الذي يُمرَّر لها كوسيط، حيث يختفي الإشعار بعد ثانية ونصف من إظهاره. وتُوفّر الخيارات التالية: // أظهِر عنصرًا يحمل النص "Hello" بالقرب من الركن العلوي الأيمن للنافذة showNotification({ top: 10, // عشرة بكسل بدءًا من أعلى النافذة والذي فاصلته 0 right: 10, // عشرة بكسل بدءًا من الحافة اليمنى للنافذة والتي ترتيبتها 0 html: "Hello!", // شيفرة HTML الخاصة بالإشعار className: "welcome" // صنف إضافي للحاوية ‘div’ (اختياري) }); استعمل تنسيقات CSS لتحديد موضع إظهار العنصر حسب الإحداثيات المعطاة بافتراض أن الصفحة تحتوي مسبقا على الأنماط التنسيقية الضرورية. الحل يمكنك الإطلاع على الحل من هنا ترجمة -وبتصرف- للفصل Styles and classes من كتاب Browser: Document, Events, Interfaces1 نقطة
-
احتاج رايكم : انا الان في وصلت لقسم VIEWS في لارافيل هل اقف واعيد المسار الجديد في اساسيات PHP او استمر في لارافيل ؟1 نقطة
-
للإضافة , ف css selectors تعمل على حسب الأولوية وتكون على الشكل التالي: - المرتبة الأولى : !important على تنسيق العنصر مثلا .className { color: "red" !important } - المرتبة الثانية : السمة style على العنصر مثلا <h1 style="color:red">Hello world</h1> - المرتبة الثالثة: التنسيق بواسطة ID #messageID{ color: "blue" } - المرتبة الرابعة التنسيق بواسطة ال class .className { color : "orange" } - المرتية الخامسة التنسيق بواسطة العنصر نفسه h1{ color: "green" } للمزيد من التفاصيل حول أولويات التحديد في css يمكنك زيارة موقع مجتمعات W3C هنا1 نقطة
-
تصميم رائع لكن يوجد خطأ في الألوان ومن الممكن أن تكون لم تتمكن من فهم الزبون بشكل كافي وجزاك الله كل خير1 نقطة
-
يجب فتح وسم php ثم طباعة هذا الكود ثم إغلاق الوسم. <?php echo ' <div class="dropdown"> <button onClick="myFunction()" class="dropbtn">Dropdown</button> <div id="myDropdown" class="dropdown-content"> <a href="#home">Home</a> <a href="#about">About</a> <a href="#contact">Contact</a> </div> </div> <script> function myFunction() { document.getElementById("myDropdown").classList.toggle("show"); } window.onclick = function(event) { if (!event.target.matches(".dropbtn")) { var dropdowns = document.getElementsByClassName("dropdown-content"); var i; for (i = 0; i < dropdowns.length; i++) { var openDropdown = dropdowns[i]; if (openDropdown.classList.contains("show")) { openDropdown.classList.remove("show"); } } } } </script> ' ?> استخدمت في عبارة echo الاقتباس الفردي ' فقمت بتغيير الاقتباس الفردي ضمن شيفرة جافاسكربت لكي لا يتم اعتبارها إغلاق لما سبق. يمكنك تجريب الكود السابق1 نقطة
-
لقد ظهر هذا الخطأ لأنك تقوم بعمل GET Request في مسار لا يقبل إلا POST. في المثال الذي تفضلت به لدينا Route::post('/settings/update', 'SettingsController@update'); إدا ذهبنا إلى المسار http://localhost:8000/settings/update فسيظهر الخطأ MethodNotAllowedHttpException لأن هذا المسار لا يقبل هذا النوع من Request والحل هو إضافة مسار اخر يقبل GET Request لإظهار form تعديل الإعدادات مثلا هكدا Route::get('/settings/create', 'SettingsController@create');1 نقطة
-
حسب المعايير القياسية المتبعة عالمياً, يعتبر دائماً اي request صادر من متصفح عبر كتابة الرابط او الضغط على رابط هو request من نوع GET اما الانواع الاخرى مثل POST و PATCH وغيرها فهي ناجمة عن الضغط على زر Submit ضمن HTML Forms لذلك فإما ان تكتب Route::get بدلاً من Route::post او يمكنك انشاء دالة جديدة Route::get بنفس المسار ونفس دالة الـCallback وتبقي Route::post على حالها او (الخيار الامثل) هو استخدام Route::any والذي يشير بقبول الـRequests من اي نوع سواء Post او Get او غيرها اذا كنت ترغب1 نقطة
-
وصلت لورا -كاتبة المقال- تعليقات إيجابية على مقالة كتبتها مسبقًا حول تجربتها في مراجعة أكثر من 200 مراسلة بين كتَّاب مستقلين وعملائهم، فجاءت الكثير من المقترحات حول كتابة مقال كامل حول ما يجب أن تفعل وما يجب ألا تفعل في العلاقات بين المستقلين والعملاء، ما الذي يجعل العميل يستمر في العمل مع نفس المستقل على المدى البعيد؟ وما الذي يجعله يرفض قائلًا "لا، شكرًا" ويمضي باحثًا عن مستقلين آخرين؟ إليك بعض القواعد للحفاظ على علاقة مستمرة مع عملائك كمستقل، فحصولك أخيرًا على مشروع الكتابة المستقل ذاك الذي عملت جاهدًا من أجله، فرصة مشوّقة وتستحقّ الاحتفال بلا شكّ، لكن عليك ألّا تتوقّف هنا. ككاتبةٍ مستقلة، متوسط مدى احتفاظ لورا بالعميل هو 14 شهرًا، اثنان من عملائها يعملون معها منذ 3 سنوات، والحقيقة هي أن الحصول على عمل والاحتفاظ بعميل مهارتان مختلفتان، وكلاهما مهمتان بنفس الدرجة للنجاح كمستقل. بالإضافة على عملها ككاتبة، عملت أيضًا كمديرة لمشروع رقمي على نطاق واسع، وعملت مع مئات المستقلّين أثناء ذلك، ولديها نصائح مهمة تساعدك لا في الحصول على العمل فقط، بل في الحفاظ عليه أيضًا، انطلاقًا من خلفيتها عن هذه المهنة. تقول لورا أنها رأت وما تزال ترى العديد من الكُتّاب المستقلّين ينسحبون لأنهم لم يلتزموا بتنفيذ الوعود التي قطعوها، وقد قالت في مقال سابق أنها عملت مع أكثر من مجموعة من المستقلّين على مشروع كان كلّ منهم مسئولًا فيه عن تسليم مقالٍ واحد أسبوعيًا، ودُفِع لعدد من الكُتّاب في بداية المشروع لقاء عملهم، لكن تمّ اقصاءهم في آخر المطاف منه. إليك بعض الأشياء التي يجب أن تفعلها أو يجب ألا تفعلها القيام عندما تبدأ علاقة عمل مع عميل جديد: اسأل أسئلة تأكد من طرحك لأيّة أسئلة توضيحيّة حول التعليمات، بيانات تسجيل الدخول، وأي موادٍّ قد لا يتوجّب على العميل رؤيتها في عيّنتك قبلَ أن تبدأ العمل، يضيع الكثير من الوقت في التعديل على أخطاءٍ كان يجب ألّا تقع في المقام الأول، كمثال جيّد، نسِيَ بعض الكُتّاب في المشروع الذي كانت تعمل لورا فيه التّعليمات الواضحة التي نصّت على إيراد عنوان فرعيّ في المقال، أجل، قد تبدو العودة إلى المقال وإضافة عنوان فرعي أو الارسال بطلب التّعديل، مشكلة بسيطة للمحرِّر أو مدير المشروع، لكن عند العمل على العشرات من المقالات في نفس الوقت، يتحوّل مصدر إزعاجٍ صغير، إلى مشكلة ضخمة تسبِّب الصُّداع، وطبعاً، تُبطئ عمليّة التّحرير، فمن الأفضل بكثير توضيح كل شيء مُسبقًا لتقليل عدد الرسائل الإلكترونية التي تُتبادل لإنجاز المشروع. لا تتعامل بدونية بالرغم من أن هذا يُفترض أن يُعلَم دون أن يُقال، كتبت كاتبة في مشروع لورا ردودًا ساخرةً على التعليقات على مستند Google الذي رفعت فيه عملها، مبدئيًا، كمديرة للمشروع، ظنّت لورا أنّها ردود بسيطة سِيْءَ فهمها، لكن عندما أتى المحرِّر وصاحِبُ الشّركة للحديث معها على انفراد وعبّرا عن انزعاجهما من الأمر، علمَت أنه سيتم اقصائها، من المؤسف ألّا يتحمل النّقد شخصٌ بقدراتٍ كتابيّة جيّدة ويتعامل بدُونِيّة وتَرَفُّع. على الرغم من أنّك قد تعتقد أنك تعرف أكثر من العميل حول عملك (أو أن العميل يرتكب أخطاءً نحويّة أو ما شابه)، إلا أن أفضل نهج هو التعامل بلطفٍ، إذا كان للعميل طريقةٌ معيّنة يفضّلها في إنجاز الأعمال، لا تحاول إعاقة ذلك بفرض ما تعتقد أنت أن عليهم أن يفعلوه، كُن احترافيًا. إذا كنت لا تتّفق مع العميل حقًا أو تشعر أنّك تُستَغَل، دَعْ تعليقًا لا يسدّ بابَ التفاهُم أو انسحب من المشروع بسلاسة، فمثلًا، لا بأس بالدّفاع عن حقّك في استخدام صيغة ما في الكتابة، إلّا إذا وضّح العميل مسبقًا في التّعليمات أنّه لا يرغب بمثل هذه الصّيغة. احترم المهلة إذا كنت تواجه صعوبة في الالتزام بالمُهلة التي حُدِّدت، تواصل مع مدير المشروع، يمكن للمُهلة أن تكون مرنة بشكل طفيف في كثير من الحالات، مع ذلك، عليك ألّا تطلب تمديدًا لها قبل انتهاءها بساعةٍ مثلًا. بدلًا من ذلك، كما هو الأمر عند طلب التّعليمات عند بداية مشروع، بيّن ما إذا كان باستطاعتك الالتزام بالمُهلة، وهذه أحد علامات الاحترافيّة في مهنة الكتابة، فإذا كنت ستعمل لهذا العميل بشكلٍ منتظم، من الأفضل أن تعرف مسبقًا ما إذا كانت المُهَل ستحدد لك يوميًا أم أسبوعيًا، لتنظّم جدولك بحيث تلتزم بها، إذا تجاوزت المُهلة، فإنّ ذلك يعطي انطباعًا للعميل بأنك غير منظّم، أو فقط لا تهتم بشأن طلباتهم ببساطة، وأنت لا تريد أيًا من هذين الانطباعَين في سمعتك. لا تنسخ أعمال الآخرين على الكلمات التي ترسلها للعميل أن تكون منك شخصيًا، باستثناء حالة حصولك على إذن لاستعمال محتوىً ما كمصدر أو نسخه ولصقه مباشرة. يُدهشني عدد الناس الذين يرسلون أعمالًا منسوخة ويتوقعون أن يفرّوا بفعلتهم. يجب أن تعلم مسبقًا أنه لن يتساهل أيّ عميل مع الأعمال المنسوخة في أيّ مشروع، الأمر يستحقُّ شراء خدمات كوبي سكيب لتوثيق أن محتواك منك أنت شخصيًا. لا تحاول أبدًا التملّص بالمحتوى المنسوخ إذ أن هذا سيؤدي إلى سلسلة من السرقات وانزعاج من جانب العميل، في حالات كثيرة، عندما عملت لورا كمديرة مشروع، أُقصِيَ من المشروع بلا تردد كل من حاولوا تقديم محتوى منسوخ ولو مرة واحدة فقط. هذا خطأ جسيم تخسر به عميلًا رائعًا، لذا ننصح بقوة بعدم فعلها، الأمر ببساطة لا يستحق. رد على طلبات التعديل في وقت مناسب لا يوجد قاعدة محددة لاتباعها عند العودة إلى عميل يطلب مراجعة جزء من العمل خلال ساعة أو نحوه، لكن في بعض الحالات، كتلك المرة عندما كانت تشرف لورا على مجموعة كُتّاب لترى كم من الوقت استغرقهم الردّ على تعليقات المحرّرين ومواصلة العمل، أمضى بعض الكاتبين خمسة أو ستّة أيام بلا أن يُشعرونا أنهم رأوا تعليق المحرّر على الأقل، فعل كهذا يتسبب في إعاقة إدارة أي مشروع. هذه مشكلة بالنسبة للعملاء حتى لو كنت الكاتب الوحيد في المشروع، لأنهم قد ينوون مراجعة العينة والموافقة عليها بعد أن تُتمّ التعديلات عليها، فعندها تتوقف العملية إلى حين إتمام التعديلات، مُعرقِلةً المشروع، وهذه ليست عادةً توَدُّ الاشتهار بها طبعًا. تأكّد من أن تضع للعميل ردًا بمُهلة معقولة فَوْر وصولك الرسالة التي يرغب فيها بالقيام بالتعديلات، لا أعتقد أنه يفترض بالعميل أن يتوقع منك أن تكون متواجدًا على البريد الإلكتروني أو على الهاتف كل ساعات اليوم، لكن من أسس الاحترافية أن تترك لهم تلميحًا ليتوقّعوا على الأقل متى سيصلهم الردّ والتّعديلات من طرفك. انسحب من المشروع بسلاسة لو اضطررت أُخرِجَ العديد من الكُتّاب في نهاية المطاف للأسف، والذين أظهروا مهارات واعدة، وغادر هؤلاء الذي تفهّموا أسباب اخراجهم بلا أيّ مشاكل، بل إنّني أرحِّب بالعمل معهم في المستقبل، لأن ذاك العمل ربّما لم يُناسبهم فقط ببساطة، بينما تعامل آخرون مع الوضع بطريقةٍ غير لائقة، مُبدين انزعاجًا، وتاركين ملاحظاتٍ لا ضرورة لها في بريدي الإلكتروني، وهذا لا يخدم أي أهدافٍ إيجابيّة وقد يُجَنّب الناس العمل معك في المستقبل. قد يَتهيأ لمثل هؤلاء الناس أن عالم تسويق المحتوى كبيرٌ جدًا بحيث يتسع لمثل هذه التعليقات، إلا أن الأمر ليس كذلك، فقد رُفضَ شخصان في بداية ذلك المشروع بعد التحقيق من ورائهما مع عُملائهما السابقين، لذا تذكّر أن التجربة التي تُخلّفها لعميل واحد يمكن أن تترك آثارًا، من الأفضل بكثير الحفاظ على احترافيّتك حتى عندما تكون مُحبَطًا. حاول فهم تعليقات المحرِّر أو مدير المشروع لتحدِّد ما عليك تحسينه للمستقبل، قد يكون منع الأخطاء من الحدوث صعبًا في بعض الحالات، لكن في حالات أخرى، من الهام تأمل الذات لرصد ما يمكن إصلاحه والقيام به بشكل أفضل في المستقبل. ونفس الشيء ينطبق على الحالات التي تنسحب فيها من المشروع لأسبابك الخاصة، لا تنسحب من مشروع في آخر دقيقة، أدِر الأمور باحترافية. تلقت أحد الكاتباتِ في ذاك المشروع الذي تحدثت عنه خلال هذا المقال، طلبَي تعديل من محرّرَين مختلفَين وفشلت في الالتزام بهما، ولم تستطع اكمال المقال أبدًا بعد ردها عليهما بعد بضعة أيام، مما أعاق الفريق واضطّرني إلى توكيل المقال إلى أحد آخر بسرعة، وحيث لم يكن يمكن إصلاح الكثير في المقال الأصلي، اضطر شخص جديد إلى البدء من جديد وتحت مُهلة أقل، سبّب هذا ضغطًا كان يمكن تجنّبه بالحفاظ على الاحترافية في العمل، إذا احتجت إلى الخروج من المشروع لأي سبب، بادر ببساطة وأعلِم أحدهم مسبقًا ولا تتركهم ينتظرون المعلومات منك، وكذلك الحال في المشروع المكتمل. فعلت احدى المحرِّرين ذلك بطريقة رائعة، حيث احتاجت إلى الانسحاب من المشروع لبضعة شهور لأسباب عائلية، وأعطتني موعدًا نهائيًا لن تتسلم من بعده أي أعمال إضافية، وسمحت لها باستكمال كل ما كان على عاتقها من أعمال مسبقًا بدون أن نُعيق تقدم المشروع، هذا منحني ومنح الفريق الفرصة والوقت للتّخطيط وإيجاد من يحلّ محلها. تعلم الممارسات الأنسب لا بد أنك ستتعلّم ما يحب العميل وما لا يحب من تعليقاته على مقالاتك أو مقالات غيرك عندما تعمل لديه لفترة طويلة. امتثل لهذا لجعل الأمور أسهل عند الكتابة لعميل، إذا كان هناك موضوع معين غير مسموح به أو كان العميل يفضّل حدًا معينًا للكلمات المفتاحية، دَوّن هذا في ملاحظاتك في المرة الأولى حتى لا تكرّر نفس الأخطاء، التمتع بمنهج متناسق في الكتابة أمرٌ شديد الأهمية مع أي عميل. ما الذي قمت أنت به لتحافظ على علاقة طويلة المدى مع عملائك؟ ما هي الأسباب التي تجعل عملائك الحاليّين يحبون العمل معك؟ مضاعفة نقاط القوة لديك تجذب المزيد من العملاء الذين يُناسبونك مثاليًا في هذه النقاط، ويؤدي إلى أسلوب حياة ممتع كمستقل. ترجمة -وبتصرف- للمقال How to Build Long-Term Freelancer-Client Relationships لصاحبته Mickey Gast1 نقطة