تخطَّ إلى المحتوى

المال والتواريخ والقرارات التي لا تُعكَس

نوعان من البيانات يقرران من مستقبل التطبيق أكثر مما يقرره الإطار. وكلاهما يُختار في الأسبوع الأول، غالبًا دون أن ينتبه أحد إلى أن قرارًا يُتخذ.

قراءة 4 دقيقة

أغلب ما يخطئ فيه تطبيق قابل للإصلاح. المتحكم السيئ يُعاد تشكيله، والاستعلام البطيء يُفهرس، والواجهة القبيحة يُعاد تصميمها.

قراران ليسا كذلك، لأنهما قراران بشأن ما كُتب. إن كانت المبالغ في قاعدة بياناتك تقريبات، فستبقى تقريبات. وإن فقد طابع زمني المعلومة اللازمة لتفسيره، فتلك المعلومة ذهبت.

وكلاهما يُتخذ في الأسبوع الأول، عادةً على يد من يكتب أول هجرة، وعادةً دون أن ينتبه أحد إلى أن قرارًا يُتخذ.

المال ليس رقمًا

الـ float لا يستطيع حمل 0.1 بدقة، لأنه ثنائي و0.1 ليست كذلك. وكل عملية حسابية تحمل خطأً صغيرًا، والأخطاء تتراكم.

$total = 0.1 + 0.2;            // 0.30000000000000004
$total === 0.3;                // false

هذه ليست غرابة في PHP؛ هكذا تعمل الفاصلة العائمة الثنائية في كل مكان. ومطبَّقة على المال تنتج فاتورة تزيد أو تنقص قرشًا، ورصيدًا لا يبلغ الصفر تمامًا أبدًا، وتقرير مطابقة لا يستطيع أحد إغلاقه.

خزّن وحدات صغرى صحيحة. المبلغ بأصغر وحدة في العملة، كعدد صحيح. لا شيء في المسار يمكن أن يكون float، فلا شيء يمكن أن ينحرف.

$table->bigInteger('amount_minor');     // 1050 = 10.50
$table->char('currency', 3);
$table->unsignedTinyInteger('currency_exponent')->default(2);

خزّن العملة، دائمًا. المبلغ بلا عملة رقم لا مال. وتعدد العملات يصل لاحقًا في منتجات أكثر مما يخطط له، وإضافة العمود بعدئذٍ تعني تقرير ما الذي كان يعنيه كل صف موجود.

خزّن الأسّ بدل افتراض اثنين. ليست كل عملة بخانتين عشريتين - بعضها بلا خانة وبعضها بثلاث. والشيفرة التي تضرب في مئة خاطئة لتلك، في الاتجاهين.

قرّر التقريب مرة، صراحةً. ضريبة على ثلاثة أسطر مقرَّبة لكل سطر تعطي مجموعًا مختلفًا عن ضريبة على المجموع. لا أحدهما خطأ؛ أما وجود الاثنين في شيفرة واحدة فخطأ. اكتب أيّهما يفعل هذا النظام.

لا تدع float يقترب. عمود decimal يُقرأ إلى float في PHP قد أبطل العمل. حوّله إلى نص أو عدد صحيح وأجرِ الحساب على الأعداد الصحيحة.

للوقت ثلاثة أشكال مختلفة

الخطأ معاملتها كنوع واحد.

لحظة - متى وقع شيء. قيمة صحيحة واحدة، تُخزَّن بـ UTC، وتُحوَّل للعرض. أُنشئ، دُفع، سجّل الدخول. هذا ما تكونه أغلب أعمدة الطوابع الزمنية وحيث يكون UTC صحيحًا بلا لبس.

تاريخ - يوم بلا وقت فيه. عيد ميلاد، أو تاريخ فاتورة، أو عطلة رسمية. تخزينه كطابع زمني يُدخل منطقة زمنية في شيء لا منطقة له، وينتج العطل الكلاسيكي حيث ينزاح تاريخ يومًا للمستخدمين على جانب من العالم.

$table->date('invoice_date');           // لا timestamp

وقت ساعة حائط مستقبلي - حدث مجدول يجب أن يقع بتوقيت محلي. هذا هو الذي ينكسر، ولهذا تكون نصيحة "خزّن بـ UTC دائمًا" ناقصة.

تذكير مضبوط للساعة 09:00 في مارس القادم، محوَّل اليوم إلى UTC، يُخزَّن كلحظة بعينها. وإن تغيّرت قواعد تلك المنطقة قبل مارس - والحكومات تغيّرها، أحيانًا بأسابيع من الإشعار - فاللحظة المخزَّنة لم تعد 09:00 محليًا. ستنطلق في الوقت الخطأ ولن يبلّغ شيء عن خطأ.

ما يجب تخزينه هو النية: الوقت المحلي، واسم المنطقة، والقاعدة. واللحظة تُحسَب حين تلزم.

$table->time('local_time');
$table->string('timezone');            // 'Europe/Istanbul'، لا '+03:00'

المنطقة اسم لا إزاحة. الإزاحة ما كانته منطقة في لحظة ما؛ والاسم هو القاعدة، والقاعدة هي ما ينجو من التغيير.

التي تُوقع الجميع

الدالة Carbon::now() تعيد المنطقة الزمنية المضبوطة في التطبيق. فإن كانت مضبوطة على شيء محلي وكان عمود مُحوَّلًا إلى datetime، فالقيمة المكتوبة وقت محلي في عمود يفترض كل شيء آخر أنه UTC.

يعمل تمامًا حتى تتغير الساعة، أو حتى يكون لخادم ثانٍ إعداد مختلف، وعندها يوجد جدول فيه نوعان من الطوابع الزمنية بلا ما يميّزهما.

اضبط منطقة التطبيق على UTC وحوّل عند الحواف - عند العرض، وعند قبول المدخلات. ومنطقة المستخدم تنتمي إلى سجل المستخدم لا إلى إعداد عام.

لماذا يستحقان جدالًا في الأسبوع الأول

كل ما عداهما في التطبيق سلوك، والسلوك يمكن استبداله. أما هذان فهما ما سُجِّل.

ترحيل عمود float إلى أعداد صحيحة يعني تقرير ما الذي كان ينبغي أن تكونه قيمة غير دقيقة، صفًّا صفًّا، وكل صف من تلك مالُ شخص ما. وطابع زمني فقد منطقته لا يمكن استنتاجها - المعلومة ليست موجودة لاستعادتها.

عشر دقائق من الاختلاف أثناء كتابة أول هجرة هي كلفة إصابة الاثنين كاملةً.

إن كانت الأعمدة خاطئة أصلًا، فالإصلاح ترحيل بيانات فيه اجتهاد، لا تغيير مخطط. هذا العمل نقوم به، ويجري بالخطوات الحذرة نفسها التي يجري بها تغيير مخطط كان سيقفل الجدول.

أسئلة ذات صلة

هل يكفي decimal في قاعدة البيانات أم نحتاج أعدادًا صحيحة؟
عمود decimal يخزّن القيمة بدقة، فقاعدة البيانات آمنة في الحالتين. والخطر في PHP: اقرأ decimal إلى float وستعود إلى حيث بدأت. الوحدات الصغرى الصحيحة تتفادى السؤال كليًا لأن لا شيء في المسار يمكن أن يكون float، ولهذا تستخدمها أغلب الأنظمة التي تتعامل مع المال بجدية.
وماذا عن عملات بلا وحدة صغرى أو بثلاث خانات؟
لذلك يجب تخزين الأسّ لا افتراضه. بعض العملات بلا تجزئة أصلًا وبعضها بثلاث خانات عشرية، فترميز "اضرب في 100" في الشيفرة خطأ في الاتجاهين. خزّن المبلغ والعملة وعدد الخانات التي تستخدمها تلك العملة.
هل يُخزَّن كل شيء بالتوقيت العالمي المنسق؟
اللحظات نعم - اللحظة التي وقعت لها تمثيل صحيح واحد وهو UTC. وما يجب ألا يُخزَّن بـ UTC هو وقت ساعة حائط لم يقع بعد، كتذكير متكرر الساعة 09:00، لأن تغيّر القواعد بين الآن وذلك الحين يجعل اللحظة المخزَّنة خاطئة.
هل يمكن إصلاحهما لاحقًا؟
يمكن ترحيلهما، وهو ترحيل بيانات فيه اجتهاد لا تغيير مخطط. تحويل float إلى أعداد صحيحة يعني تقرير ما الذي كان ينبغي أن تكونه قيمة لم تكن دقيقة قط، وكل واحد من تلك القرارات مالُ شخص ما.

← العودة إلى كل المقالات

اتصل بنا+1 848 272 7583واتساب+90 850 308 5436البريدinfo@codefacture.comصفحة التواصل