PHP Best Practices بالعربي

Standard

مقدمة بسيطة كده

PHP واحدة من اللغات اللي تتطوت على مدار السنين وكل تحديث او نسخة جديدة تنزل بيبقة فيها جديد وتطوير. المشكلة لانها لغة سهلة ومن نوع Weakly-typed مش Strongly-typed زي JAVA او C# مثلا … فا دا بيأدي لنوع من البرمجة الرقيقة او اللي فيها “عك” شوية بمعنى اصح … الموضوع ده موجود في كل اللغات مش في ال PHP فقط … بس اللغات اللي زيها بيبقة الموضوع رخم حبتين خصوصا في الأول وبتبان قوي لو واحد لسة بيتعلم برمجة وبيطبق على لغة زي PHP او Python.

وعشان الموضوع يبقة مفهوم اكتر اللغات اللي زي PHP و Python و Scala معملوه بمفهوم انها متجبرش اللي يشغل بيها على حاجة يعني خليه برحته يا برنس ومتخنقش عليه. بس ده على اساس انه فاهم Programming Concept كويس على الاقل.

في الاول والاخر لغة البرمجة دي بتبقة عبارة عن أداة بيستخدمها المبرمح عشان يعمل بيها الحاجة اللي عاوزها. فمينفعش تقول اللغة الفلانية احسن من اللغة العلانية لمجرد انك شوفت حاجة معمولة باللغة العلانية دي وكانت بايظه او بطيئة.

عشان كده لازم يبقة في زي قواعد او اساسيات الناس تتفق عليها عشان يبقة جودة الحاجة اللي بتتعمل كويسة من كل النواحي. القواعد دي الـ community هو اللي حطها ودي ميزة مهمة جدا جدا لانها بتبقة نتيجة خبرة ناس داست جامد في الشغل باللغة وكمان انت مش ملزم تلتزم بيها خالص وتعرف تلعب برحتك … بس اعمل حسابك ان في احتمال كبير جدا انك مش الوحدي اللي هتشتغل على ال project وغير كده كمان انت ممكن تعمل حاجة وتيجي تبص عليها بعد شهر واحد متفهمش حاجة وتحس ان في حد جه من المريخ سيطر على دماغك 😀 … عشان كده برده موضوع ان الواحد يمشي على قواعد واساسيات معينة كويس وفي نفس الوقت ممكن تغير فيها بشكل يناسبك ويناسب غيرك.


PHP version اللي بتستخدمها ايه ؟؟؟

حسب اخر احصائيات للنسخ اللي منتشرة والمستخدمة على ال Servers دلوقتي (حسب وقت كتابة المقال) إن 5.3 هو الأكثر انتشاراً بنسبة 48.9% … فا ده معناه انك لازم في شغلك تبقة عمال حساب كده لان زي ما قولنا كل نسخة جديدة فيها حاجات جديدة وحاجات اتعمل Deprecate وغيره … هو المفضل دائما انك تشتغل بأخر حاجة نازلة Stable وهو دلوقتي 5.6.0 … بس زي ما شايفين في الاحصائية ان استخدام اخر نسخة اقل من 1% ودا راجع لأنها لسة نازلة في شهر 8 / 2014 غير ان في Shared hosting كتير بيتأخروا في عمل update لل packages بتاعهتا.

عشان كده بينصح انك تشتغل بنسحة متكونش متأخر عن اخر حاجة نازلة كتير … يعني اشتغل بنسخة 5.4.x أو 5.5.x وتبقة ال Development Environment اللي شغال عليها قريبة على قد ما تقدر من ال Production عشان تعرف تعمل محاكلة لكل حاجة … بمعنى أخر حاول تجرب حاجاتك على Linux Server عشان تقريبا 67.7% من ال Servers اللي هيشتغل علها ال App بتاعك هيبة شغال Linux Ubuntu او Linux CentOS مثلا … عشان كده حاول على قد ما تقدر تعمل محاكاه وممكن حتى بـ Virtual Machine وفي منها كتير زي VMware او VirtualBox


Password hashing … اوعا تسجل ال Passwords زي ما هي كده .. هدبحك !!!

لازم لازم لازم ولازم كمان مرة تعمل Hashing لل Passwords قبل ما تسجلها في ال Database لأنها مسؤلية كبيرة جدا وانت المسؤل عن حماية بيانات الـ User … في طرق كتيرة عشان تعمل Hashing اقدمهم حاجات زي md5 و sha1 ودول عفى عليهم الزمن خلاص معدوش أمنين باي حاجة من الاحوال … PHP من اول 5.5 فيها built-in hashing بإستخدام bcrypt algorithm وتعتبر دي دلوقتي الطريقة المتبعة والمفضلة … لو عندك انت Algorithm تانية احسن تبقة معلم 😀

<?php

// if php version >= 5.5
if ( version_compare( phpversion(), '5.5', '>=' ) ) 
{
    // use built-in library

    // Hash the password.  $hashed_password will be a 60-character string.
    $hashed_password = password_hash( 'my super cool password', PASSWORD_DEFAULT );

    // You can now safely store the contents of $hashed_password in your database!

    // Check if a user has provided the correct password by comparing what they typed with our hash
    password_verify( 'the wrong password', $hashed_password ); // false
    password_verify( 'my super cool password', $hashed_password ); // true
}
else
{
    // older version
    // use phpass library http://www.openwall.com/phpass/

    // Include the phpass library
    require_once( 'phpass-0.3/PasswordHash.php' );

    // Initialize the hasher without portable hashes (this is more secure)
    $hasher = new PasswordHash( 8, false );

    // Hash the password.  $hashed_password will be a 60-character string.
    $hashed_password = $hasher->HashPassword( 'my super cool password' );

    // You can now safely store the contents of $hashed_password in your database!

    // Check if a user has provided the correct password by comparing what they typed with our hash
    $hasher->CheckPassword( 'the wrong password', $hashed_password ); // false
    $hasher->CheckPassword( 'my super cool password', $hashed_password ); // true
}
?>

بتعمل Connect على ال MySQL Database ازاي ؟؟؟ ازعل منك لو قولتلي mysql driver 🙁

في 3 انواع لل database drivers موجودين built-in في ال PHP هما mysql و mysqli و PDO اقدمهم هي ال mysql اللي كان عملها الفريق اللي عمال MySQL اساسا بس التطوير فيها وقف من زمان ومعدش بيحصل فيهل اي تحديث وكده معناها انها ماتت ولازم تستخدم بديل اللي هما mysqli أو PDO وخصوصا تستخدم ال Prepare Statements بتاعتهم والحتة دي مهمة جدا جدا … الاتنين جامدين وحلوين الفرق بنهم إن ال PDO بتدعم انواع تانية من ال Databases غير MySQL عكس mysqli اللي بتشتغل مع MySQL بس … هتستخدم انهي فيهم بقة دي حاجة ترجعلك وترجع لاحتياجك واحتياج ال App اللي شغال عليه.

<?php

/**
 * MySQLi Driver
 */

$mysqli = new mysqli( 'localhost', 'my_user', 'my_password', 'world' );

/* check connection */
if ( $mysqli->connect_errno )
    die('Connect failed: %s\n', $mysqli->connect_error);

$query = 'SELECT ID, user_login, user_email FROM users ORDER by ID LIMIT 3';
$result = $mysqli->query( $query );

/* numeric array */
$row = $result->fetch_array( MYSQLI_NUM );
printf ( '%s (%s)\n', $row[0], $row[1] );

/* associative array */
$row = $result->fetch_array( MYSQLI_ASSOC );
printf ( '%s (%s)\n', $row['ID'], $row['user_login'] );

/* associative and numeric array */
$row = $result->fetch_array( MYSQLI_BOTH );
printf ( '%s (%s)\n', $row[0], $row['user_login'] );

/* free result set */
$result->free();

/* close connection */
$mysqli->close();

/**
 * PDO Driver
 */
try
{
    // connect to Database
    $db_connection = new \PDO( 'mysql:host=your-hostname;dbname=your-db;charset=utf8mb4', 
                        'your-username', 
                        'your-password', 
                        array (
                            \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, 
                            \PDO::ATTR_PERSISTENT => false
                        )
                    );
    
    // data placeholders '?'
    $handle = $db_connection->prepare( 'select ID, user_login, user_email from users where ID = ? or user_email = ? limit ?' );

    // fill placeholders with data in order
    $handle->bindValue( 1, 100, PDO::PARAM_INT ); // cast and escape data to number integer
    $handle->bindValue( 2, 'mail@yahoo.com' ); // cast and escape by default to string
    $handle->bindValue( 3, 5, PDO::PARAM_INT );
 
    // run the query
    $handle->execute();

    // fetch the data set returned as array or objects
    $result = $handle->fetchAll( \PDO::FETCH_OBJ );

    foreach( $result as $row )
    {
        print( $row->user_login );
    }
}
catch( \PDOException $ex ) 
{
    print( $ex->getMessage() );
}
?>

PHP tags … الاختصارات No

دائما استخدم Full tags … يعني بلاش تستخدم الاختصارات او ال ASP style tags … دائما استخدم <?php ?> بلاش الحاجات التانية زي <?= ?> … وكمان بلاش تقفل PHP tag في اخر الصفحة عشان ميبقال اي حاجة تتبعت في ال Header بتاع ال صفحة بالغلط زي newline character او space … الحاجات دي ممكن تلخبطك وتعمل ازمة لو بتعمل URL redirect يطلعلك header already sent error


Autoloading Classes … ريح نفسك وريح غيرك

تقدر تعمل Autoload للـ Classes files في ال PHP بكذا طريقة. اقدم طريقة هي استخدام الـ Magic Function اللي إسمها __autoload ومشكلتها انها بتسمحش انك تعمل كذا Autoload في نفس الوقت ودي مشكلة لما الكود بتاعك يشتغل مع كود حد تاني وبيحصل Conflict. عشان كده استخدم spl_autoload_register اللي بتخليك تقدر تعمل كذا autoload من غير مشكلة وبـ unique name عشان المشكلة دي متحصلش معاك.

<?php
// First, define your auto-load function.
function wp_classes_loader( $class_name )
{
    // the target class name will be passed as the first argument
    // In this example, all classes files places in a "wp-include" directory
    // with file name prefix of "class-"
    // ex: "WP_User" class file path will be "wp-include/class-wp-user.php"
    $path = 'wp-include/class-'. str_replace( '_', '-', strtolower( $class_name ) ) .'.php';

    // check class file first
    if ( file_exists( $path ) )
    {
        // use require_once or include_once to handle same class multiple calls
        require_once $path;
    }
}
 
// Next, register it with PHP.
spl_autoload_register( 'wp_classes_loader' );
 
// create instance of your class
$new_user = new WP_User();

Single vs Double quotes performance … مش فارقة قوي معظم الاحيان

الموضوع ده يرجع للفرق في طريقة الشغل بين Single quotes و ال Double quotes … الـ String بين الـ Double Quotes هيتعمله parsing ولو حاطت فيه $variable هيعتعمله evaluation ودا طبعا معناه process زيادة … الفكرة ان ال process دي مش بتاخد resources كتير … هتفرق معاك في الـ Large Scale Apps … الـ Benchmark اللي معمول ده بيوضح الفرق في لما الحجم ال Data بيكبر … مش هتخسر حاجة لو استخدمت Single quotes على قد ما تقدر مدام مش هتأثر على ال App Logic … كل ما كان ال App بيستخدم resources أقل كل ما كان احسن في كل الأحوال.


define() Vs const … في expressions اديله define() مفيش expressions اديله const

برده الموضوع ده يرجع للفرق في طريقة الشغل الاتنين … define بتعرف الـ constant في الـ runtime انما const بتعرف الـ constant في الـ compile ودا طبعا بيدي افضلية للـ const بس المشكلة في const انك متعرفش تستخدم اي expression ولازم تديله Fixed Value من غير ما تعمل عيلها حاجة ( من أول PHP 5.6 اتضاف انك تستخدم expression مع const ) إنما define تقدر تعمل اللي انت عاوزه … عشان كده define مرنة اكتر … بس برده في مشكلة تانية انك متعرفش تعرف constant خاص ب namespace معين بـ define … لازم تستخدم const … وعشان كده انت تستخدم انهي هترجعلك وهترجع لاحتياجك في الـ App عامل ازاي.

<?php
namespace Company\Departments\Accounting;

const PRINTER_ID = 1;

define( 'Company\Departments\Sales\PRINTER_ID', 2 );
 
echo \Company\Departments\Accounting\PRINTER_ID;  // 1
echo \Company\Departments\Sales\PRINTER_ID;  // 2

// bit-shifted constants
// OK!
define( 'BIT_CONST', 1 << 0 );
// Compile error! const can't use expressions as values
const OTHER_BIT_CONST = 1 << 1;
 
// Next, conditional constants.
define( 'HOBBITS_FRODO_ID', 1 );

// OK
define( 'COPY_CONST', BIT_CONST );
// Compile error: const can't be used in an if block
const OTHER_COPY_CONST = BIT_CONST;
 
// Finally, class constants
class WP_USER
{
    // OK
    const table = 'wp_users';

    // Compile error: can't use define() within a class
    define( 'INCREMENT', 4 );
}

Regular Expression … متعملش كل حاجة بيها وخلاص

بالنسبة لموضوع ال regex دي حاجة اساسية بس خد بالك انه engine لوحدة جوه ال engine بتاع ال PHP يعني لما تستخدم regex حاول على قد ما تقدر تستخدمها بعقل كده وتركيز ولو تعرف تعمل نفس الحاجة من غير ما تستخدمها يبقة احسن … ومتنساش تستخدم ال functions اللي بتبتدي ب preg_ بس لان الباقي بقة Deprecated من اول version 5.3

<?php

// Some string
$text = 'Humans are creative beings, The standard default text is designed to ramble about nothing, Humans are creative beings. The standard default text is designed to ramble about nothing.';

// replace spaces, "," comma, "." dot with underscore
// using regex
$text = preg_replace( '/[\s\.,]/', '_', $text );

// using str_replace
// faster as it won't use regex engine
$text = str_replace( array( ' ', '.', ',' ), '_', $text );

 


بتعمل Serving PHP من على الـ Web Server ازاي ؟ لو mod_php “اجري يا ماجدي”

النقطة دي خاصة اكتر بـ Server Administrators فا لو مفهمتاش قوي قول للـ tech support بتوع الServer وهم هيظبطوها … المهم … في كذا طريقة عشان الـ Web server زي Apache يشغل الـ PHP code بتاعك mod_php و mod_fastcgi و mod_fcgid … اقدمهم mod_php وهي ابطئهم لنها بتخلي الـ Apache هو اللي يتحكم في PHP process ودا مش كويس لأنه مبيعرفش يظبط الـ Memory Handling كويس مع الـ PHP لما الـ traffic يعلى … عشان كده استخدم اي طريقة من التنين وفي طريقة احسن كمان منهم بس معقدة شوية بس نتيجتها حلوة جدا جدا وهي PHP-FPM ودي اساساً مبنية على mod_fastcgi وميزتها انها بتتحكم في عدد ال PHP process اللي شغالة وبيحرجمها حسب الاحتباج وبيبعدها عن ال Web server خالص وبيخلي ال Apache ينده عليه عن طريق socket server … ودي ميزة تانية مهمة كمان عشان تقدر تتحكم في ال PHP Process زي منتا عاوز بعيد عن الـ Web Server


بتبعت اميلات ازاي ؟؟؟ هتقولي mail() هزعل منك قوي

لازم نعترف ان PHP مش لغة كاملة وفيها شوية حاجات غبية حبتين ومنهم انك تستخدم mail() built-in function عشان تبعت email … مشكلتها انها بستتخدم resources كتير وممتعرفش تحكم بشكل كامل عليها … عشام كده استخدم حاجة زي PHPMailer كويسة جدا وكتير جدا معتمدين عليها لأنها library فيها options كتير غير انها شغالة OOP Interface وتقدر من خلالها تبعت mails بكميات كبيرة ومن على mail servers تانية وتعمل connection عن طريق SMTP Protocol over SSL or TLS.

<?php
// Include the PHPMailer library
require_once 'phpmailer-5.2.7/PHPMailerAutoload.php'ك
 
// Passing 'true' enables exceptions.  This is optional and defaults to false.
$mailer = new PHPMailer( true );
 
// Set up to, from, and the message body.  The body doesn't have to be HTML; check the PHPMailer documentation for details.
$mailer->Sender = 'bbaggins@example.com';
$mailer->AddReplyTo( 'bbaggins@example.com', 'Bilbo Baggins' );
$mailer->SetFrom( 'bbaggins@example.com', 'Bilbo Baggins' );
$mailer->AddAddress( 'gandalf@example.com' );
$mailer->Subject = 'The finest weed in the South Farthing';
$mailer->MsgHTML( '<p>You really must try it, Gandalf!</p><p>-Bilbo</p>' );
 
// Set up our connection information.
$mailer->IsSMTP();
$mailer->SMTPAuth = true;
$mailer->SMTPSecure = 'ssl';
$mailer->Port = 465;
$mailer->Host = 'my smtp host';
$mailer->Username = 'my smtp username';
$mailer->Password = 'my smtp password';
 
// All done!
$mailer->Send();


Email Address Sanitizing and validation … على طول كده filter_var()

اكيد عادى عليك وهيعدي عليه انك تعمل validate لـ Email address واكيد دورت كتير وهتلاقي Regex طويلة عريضة كده تخدها copy & paste عشان تعمل بيها validate … طيب بدل ما تدور وتلف ما في الـ PHP طريقة مضمونة عشان تعمل بيها Data filtering ومنها filter_var().

<?php
// return email address string
filter_var( 'bob@example.com', FILTER_VALIDATE_EMAIL );

// returns false
filter_var( 'bob@example', FILTER_VALIDATE_EMAIL );


Sanitizing HTML input and output … اوعى من XSS Attack

عشان تريح نفسك من وجع دماغ ال XSS ولان الـ User دائما على خطأ عشان كده اي input من ال User مشكوك فيه والـ output كمان. فا على طول تستخدم htmlentities() لو هتعمل simple sanitizing بس لو هتحتاج حاجة معقدة شوية اكتر ممكن تستخدم اي library حد عملها اشهرهم في النقطة دي هي HTML Purifier

<?php

// Built-in API
$html = '<div onclick="xss();">Mua-ha-ha!  Twiddling my evil mustache...</div>';

// Use the ENT_QUOTES flag to make sure both single and double quotes are escaped.
// Use the UTF-8 character encoding if you've stored the text as UTF-8 (as you should have).
$safe_html = htmlentities( $html, ENT_QUOTES, 'UTF-8' );

// Include the HTML Purifier library
require_once 'htmlpurifier-4.6.0/HTMLPurifier.auto.php';
 
// Set up the HTML Purifier object with the default configuration.
$purifier = new HTMLPurifier( HTMLPurifier_Config::createDefault() );
 
$safe_html = $purifier->purify( $evilHtml );


UTF-8 Encoding … أه منك انت أه

جينا للرخامة بعينها في ال PHP … لغاية دقوتي اللغة مفيهاش Low level support for Unicode دي مشكلة رخمة جدا جدا … بس حلها سهل او بمعنى اخر بقى سهل 😀 … المشكلة انها على كذا مرحلة او Level … الـ Browser و MySQL و PHP … المشكلة دي دايما بتظهر مع اللغات الشرقية واللغات اللي مش مبنية على اللاتيني Non-latin based languages

بالنسبة لل PHP حاول على قدر الامكان تستخم Multibyte String operations هي هتتطلب تعديل شوية في الكود بس حاجات بسيطة وهي built-in وخلي ملف ال PHP نفسه اللي بتكتب فيه ال charset بتاعه UTF-8

بالنسبة للـ MySQL خلي دايما charset بتاع الـ database connection بـ utf8mb4 والـ database نفسها UTF-8

بالنسبة لل Browser اتأكد ان في meta tag بالـ character encoding بيساوي UTF-8 وفي نفس الوقت اللي هتبعته للـ browser كـ HTML من الـ PHP اعمل قبل ما تبعته mb_http_output عشان تقول لل browser ان الـ response content character encoding بـ UTF-8

<?php
// Tell PHP that we're using UTF-8 strings until the end of the script
mb_internal_encoding( 'UTF-8' );
 
// Tell PHP that we'll be outputting UTF-8 to the browser
mb_http_output( 'UTF-8' );
 
// Our UTF-8 test string
$string = 'مصر';

// Transform the string in some way with a multibyte function
// Note how we cut the string at a non-Ascii character for demonstration purposes
$string = mb_substr( $string, 0, 15 );

// Connect to a database to store the transformed string
// See the PDO example in this document for more information
// Note that we define the character set as utf8mb4 in the PDO connection string
$link = new \PDO( 'mysql:host=your-hostname;dbname=your-db;charset=utf8mb4',
                    'your-username',
                    'your-password',
                    array (
                        \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
                        \PDO::ATTR_PERSISTENT => false
                    )
        );

// Store our transformed string as UTF-8 in our database
// Your DB and tables are in the utf8mb4 character set and collation, right?
$handle = $link->prepare( 'insert into countries (ID, name) values (?, ?)' );
$handle->bindValue( 1, 1, PDO::PARAM_INT );
$handle->bindValue( 2, $string );
$handle->execute();

// Retrieve the string we just stored to prove it was stored correctly
$handle = $link->prepare( 'select * from countries where Id = ?' );
$handle->bindValue( 1, 1, PDO::PARAM_INT );
$handle->execute();

// Store the result into an object that we'll output later in our HTML
$result = $handle->fetchAll( \PDO::FETCH_OBJ );

// doctype right after php closing tag
?><!doctype html>
<html>
    <head>
        <meta charset="UTF-8" />
        <title>UTF-8 test page</title>
    </head>
    <body>
        <?php
        foreach( $result as $row )
    {
            echo $row->name, '<br/>';
        }
        ?>
    </body>
</html>

التعامل مع التوقيت والتاريخ … كانت ايام فيها Twist

كان قبل كده عشان تتعامل مع التوريخ والتوقيت في الـ PHP كانت لبخة شوية وكنت تطر تستخدم function كتير مع بعض dategmdate, date_timezone_set, strtotime وغرهم … بس دلوقتي فيه built-in Class بتسهل الموضوه كتير جدا وسهل في الاستخدام DateTime class هي من جواها اساسا بتستعمل ال functions القديمة لنها فعالة جدا وسريعة وزيادة عليهم حاجات تانية … ونصيحة حاول تتعامل مع الحاجات القديمة للفهم مش اكتر عشان هي low level شوية.

<?php
// Construct a new UTC date.  Always specify UTC unless you really know what you're doing!
$date = new DateTime( '2011-05-04 05:00:00', new DateTimeZone( 'UTC' ) );

// Add ten days to our initial date
$date->add( new DateInterval( 'P10D' ) );

echo $date->format( 'Y-m-d h:i:s'); // 2011-05-14 05:00:00

// Sadly we don't have a Middle Earth timezone
// Convert our UTC date to the PST (or PDT, depending) time zone
$date->setTimezone( new DateTimeZone( 'America/Los_Angeles' ) );

// Note that if you run this line yourself, it might differ by an hour depending on daylight savings
echo $date->format( 'Y-m-d h:i:s' ); // 2011-05-13 10:00:00

$later = new DateTime( '2012-05-20', new DateTimeZone( 'UTC' ) );

// Compare two dates
if ( $date < $later )
    echo 'Yup, you can compare dates using these easy operators!';

// Find the difference between two dates
$difference = $date->diff( $later );

echo 'The 2nd date is ', $difference['days'] ,' later than 1st date.';


“==” vs “===” مع Null و Boolean … هتشد في شعرك او استخدمت “==”

من ضمن الحاجات الغريبة في اللغات ال Weakly Typed انها بتساوي حاجات ببعضها مش منطقية خالص زي مثلا لو قارنت بين empty string و NULL هيديك true !!!! ازاي معرفش …. عشان كده لو المقارنات فيها null او Boolean او الـ Data type هتفرق معاك يقة لازم تستخدم “===” strict equivalence operators و “!==

<?php

echo grab_dump( 1 == '1' ); // true
echo grab_dump( 1 === '1' ); // false
echo grab_dump( true == '1' ); // true
echo grab_dump( false == '' ); // true
echo grab_dump( false === '' ); // false
echo grab_dump( 0 == false ); // true
echo grab_dump( 0 === false ); // false
echo grab_dump( 0 == '' ); // true
echo grab_dump( 0 === '' ); // false
echo grab_dump( 0 == null ); // true
echo grab_dump( 0 === null ); // false
echo grab_dump( false == null ); // true
echo grab_dump( false === null ); // false

function grab_dump( $var )
{
    return $var === true ? 'true' : 'false';
}

A contribution for PHPBestPractices.org

2 thoughts on “PHP Best Practices بالعربي

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.