ملاحظات للعاملين بلغة php معالجة الملفات والبيانات المرمزة بترميز UTF-8 في PHP


سارة محمد2

دوال الملاءمة (Convenience functions)

الخرج والدخل الخام المباشر

توفر الدالتان file_get_contents و file_put_contents قابلية القراءة/الكتابة إلى/من ملف من/إلى سلسلة نصية في PHP في استدعاء واحد.

يمكن أن تستخدم الدالة file_put_contents أيضًا مع راية القناع البتي FILE_APPEND للإضافة إلى الملف بدلًا من الكتابة فوقه، ويمكن استخدامها مع القناع البتي LOCK_EX للحصول على قفل حصري للملف أثناء إجراء الكتابة. يمكن دمج رايات القناع البتي باستخدام عامل العملية الثنائية OR |.

$path = "file.txt";

// ‫قراءة محتويات الملف file.txt إلى ‎$contents
$contents = file_get_contents($path);

// ‫إذا غيّرنا شيئًا ما مثلًا `CRLF` إلى `LF`
$contents = str_replace("\r\n", "\n", $contents);

// ‫نكتب هذا التغيير في file.txt مستبدلين المحتويات الأصلية
file_put_contents($path, $contents);

تفيد الراية FILE_APPEND في الإضافة إلى ملفات السجلات، بينما تساعد الراية LOCK_EX في منع حالة سباق عدة عمليات للكتابة في الملف، فمثلًا لكتابة حالة الجلسة الحالية في ملف سجل:

file_put_contents("logins.log", "{$_SESSION["username"]} logged in", FILE_APPEND | LOCK_EX);

الخرج والدخل بصيغة CSV

fgetcsv($file, $length, $separator)

تحلل الدالة fgetcsv سطرًا من الملف المفتوح وتبحث عن حقول csv، ثم تعيد هذه الحقول في مصفوفة في حالة النجاح وإلا تعيد FALSE. تقرأ بشكلٍ افتراضي سطرًا واحدًا فقط من ملف csv.

$file = fopen("contacts.csv","r");
print_r(fgetcsv($file));
print_r(fgetcsv($file,5," "));
fclose($file);

محتويات ملف contacts.csv:

Kai Jim, Refsnes, Stavanger, Norway
Hege, Refsnes, Stavanger, Norway

الخرج:

Array
(
    [0] => Kai Jim
    [1] => Refsnes
    [2] => Stavanger
    [3] => Norway
)
Array
(
    [0] => Hege,
)

قراءة ملف إلى مجرى الخرج القياسي مباشرةً

تنسخ الدالة readfile ملفًا إلى المخزن المؤقت للخرج، ولا تؤدي إلى ظهور أيّة مشاكل في الذاكرة حتى عند إرسال ملفات كبيرة من تلقاء نفسها.

$file = 'monkey.gif';

if (file_exists($file)) {
    header('Content-Description: File Transfer');
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename="'.basename($file).'"');
    header('Expires: 0');
    header('Cache-Control: must-revalidate');
    header('Pragma: public');
    header('Content-Length: ' . filesize($file));
    readfile($file);
    exit;
}

القراءة بدءًا من مكان مؤشر في ملف

نستخدم الدالة fpassthru بدلًا من ذلك للبحث عن نقطة في الملف للبدء بالنسخ إلى مجرى الخرج القياسي، في المثال التالي يُنسخ آخر 1024 بايت إلى الخرج القياسي:

$fh = fopen("file.txt", "rb");
fseek($fh, -1024, SEEK_END);
fpassthru($fh);

قراءة ملف إلى مصفوفة

تعيد الدالة file أسطر الملف الممرَّر في مصفوفة، يقابل كل عنصر من المصفوفة سطرًا من الملف مع استمرار إضافة السطر الجديد.

print_r(file("test.txt"));

محتويات الملف test.txt:

Welcome to File handling
This is to test file handling

الخرج:

Array
(
    [0] => Welcome to File handling
    [1] => This is to test file handling
)

حذف ملفات ومجلدات

حذف ملفات

تحذف الدالة unlink ملفًا واحدًا وتعيد القيمة TRUE عند نجاح العملية وFALSE عند فشلها.

$filename = '/path/to/file.txt';

if (file_exists($filename)) {
    $success = unlink($filename);
    if (!$success) {
        throw new Exception("Cannot delete $filename");
    }
}

حذف مجلدات مع حذف عودي (recursive deletion)

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

يفحص المثال التالي الملفات في مجلد ويحذف ما بداخله من ملفات ومجلدات بشكلٍ تعاودي ويعيد عدد الملفات (وليس المجلدات) المحذوفة.

function recurse_delete_dir(string $dir) : int {
    $count = 0;

    // ‫التأكد من أنّ ‎$dir ينتهي بخط مائل لنتمكن من ربطه مع أسماء الملفات مباشرةً
    $dir = rtrim($dir, "/\\") . "/";

    // ‫استخدام dir()‎᠎ للحصول على قائمة الملفات
    $list = dir($dir);

    // ‫تخزين اسم الملف التالي في ‎$file، إذا كانت قيمة ‎$file هي false أي لا توجد ملفات بعد عندها تُنهى الحلقة
    while(($file = $list->read()) !== false) {
        if($file === "." || $file === "..") continue;
        if(is_file($dir . $file)) {
            unlink($dir . $file);
            $count++;
        } elseif(is_dir($dir . $file)) {
            $count += recurse_delete_dir($dir . $file);
        }
    }

    // يمكننا الآن حذف المجلد
    rmdir($dir);
    return $count;
}

الحصول على معلومات ملف

التحقق من كون المسار ملف أو مجلد

تتحقق الدالة is_dir إن كان الوسيط الممرر إليها مجلدًا أم لا، فيما تتحقق الدالة is_file إن كان الوسيط الممرر إليها ملفًا أم لا، ويمكن استخدام الدالة file_exists للتحقق من وجود ملف أو مجلد محدد.

$dir = "/this/is/a/directory";

$file = "/this/is/a/file.txt";

echo is_dir($dir) ? "$dir is a directory" : "$dir is not a directory", PHP_EOL,
    is_file($dir) ? "$dir is a file" : "$dir is not a file", PHP_EOL,
    file_exists($dir) ? "$dir exists" : "$dir doesn't exist", PHP_EOL,
    is_dir($file) ? "$file is a directory" : "$file is not a directory", PHP_EOL,
    is_file($file) ? "$file is a file" : "$file is not a file", PHP_EOL,
    file_exists($file) ? "$file exists" : "$file doesn't exist", PHP_EOL;

الخرج:

/this/is/a/directory is a directory
/this/is/a/directory is not a file
/this/is/a/directory exists
/this/is/a/file.txt is not a directory
/this/is/a/file.txt is a file
/this/is/a/file.txt exists

التحقق من نوع ملف

نستخدم الدالة filetype لمعرفة نوع ملف محدد والذي قد يكون إمَّا أنبوبة مسماة FIFO أو ملف محرفي خاص char أو مجلد dir أو ملف كتلي خاص block أو وصلة رمزية link أو ملف عادي file أو مقبس socket أو نوع غير معروف unknown.

تمرير اسم الملف إلى الدالة filetype مباشرةً:

echo filetype("~"); // dir

لاحظ أنّه إذا لم يكن الملف موجودًا فإنّ الدالة تعيد false وتشغّل E_WARNING.

التحقق من قابلية القراءة والكتابة

نمرر اسم الملف للدالتين is_writable وis_readable للتحقق من كون الملف قابلًا للكتابة أو القراءة.

تعيد الدالتان القيمة false إذا لم يكن الملف موجودًا.

التحقق من وقت الوصول إلى ملف وتعديله

تعيد الدالتان filemtime وfileatime الختم الزمني لآخر تعديل أو وصول للملف، ويكون الختم الزمني المعاد بصيغة Unix.

echo "File was last modified on " . date("Y-m-d", filemtime("file.txt"));
echo "File was last accessed on " . date("Y-m-d", fileatime("file.txt"));

الحصول على أجزاء المسار مع fileinfo

$fileToAnalyze = ('/var/www/image.png');

$filePathParts = pathinfo($fileToAnalyze);

echo '<pre>';
print_r($filePathParts);
echo '</pre>';

خرج المثال السابق:

Array
(
    [dirname] => /var/www
    [basename] => image.png
    [extension] => png
    [filename] => image
)

يمكن استخدام هذا الخرج كالتالي:

$filePathParts['dirname']
$filePathParts['basename']
$filePathParts['extension']
$filePathParts['filename']
المعامل تفاصيل
‎$path المسار الكامل للملف الذي نريد تحليله
‎$option ‫أحد الخيارات التالية: PATHINFO_DIRNAME أو PATHINFO_BASENAME أو PATHINFO_EXTENSION أو PATHINFO_FILENAME
  • تُعاد مصفوفة ترابطية (associative array) إذا لم يُمرَّر المعامل الثاني وإلا تُعاد سلسلة نصية.
  • لا تتحقق من وجود الملف
  • تُحلَّل السلسلة النصية ببساطة إلى أجزاء ولا يُجرى أي تحقق على الملف (مثل التحقق من نوع الترويسة وغير ذلك)
  • الامتداد هو الامتداد الأخير للمتغير ‎$path، سيكون مسار الملف image.jpg.png هو ‎.‎png حتى لو كان هو ملف ‎.‎jpg تقنيًا، وإذا كان الملف بدون امتداد لن يُعاد عنصر الامتداد في المصفوفة.

خرج ودخل ملف معتمد على المجرى

فتح مجرى

تفتح الدالة fopen ملفًا وتخصص له مقبضًا يشبه مقبض الباب يمكن أن تستخدمه دوالًا أخرى للقراءة والكتابة والبحث وغيرها، هذه القيمة من النوع مورد ولا يمكن تمريرها إلى النياسب الأخرى المستمرة في وظيفتها.

$f = fopen("errors.log", "a");

ستحاول الشيفرة السابقة فتح الملف errors.log للكتابة.

المعامل الثاني هو نمط مجرى الملف:

النمط الوصف
r يفتح الملف للقراءة فقط بدءًا من أول الملف
r+‎ يفتح الملف للقراءة والكتابة بدءًا من أول الملف
w يفتح الملف للكتابة فقط بدءًا من أول الملف، إذا كان الملف موجودًا سيفرغه وإذا لم يكن موجودًا سيحاول أن ينشئه
w+‎ يفتح الملف للقراءة والكتابه بدءًا من أول الملف، إذا كان الملف موجودًا سيفرغه وإذا لم يكن موجودًا سيحاول أن ينشئه
a يفتح الملف للكتابة فقط بدءًا من نهاية الملف، إذا لم يكن الملف موجودًا سيحاول أن ينشئه
a+‎ يفتح الملف للقراءة والكتابة بدءًا من نهاية الملف، إذا لم يكن الملف موجودًا سيحاول أن ينشئه
x ‫ينشئ ويفتح ملف للكتابة فقط، وإذا كان الملف موجودًا ستفشل الدالة fopen ‏
x+‎ ‫ينشئ ويفتح ملف للكتابة فقط، وإذا كان الملف موجودًا ستفشل الدالة fopen ‏
c يفتح الملف للكتابة فقط، إذا لم يكن موجودًا سيحاول أن ينشئه، يبدأ الكتابة من أول الملف لكنه لا يفرغ الملف قبل الكتابة
c+‎ يفتح الملف للقراءة والكتابة، إذا لم يكن موجودًا سيحاول أن ينشئه، يبدأ الكتابة من أول الملف لكنه لا يفرغ الملف قبل الكتابة

إنّ إضافة المحرف t بعد النمط (مثل a+b، ‏wt وغير ذلك) في نظام التشغيل ويندوز ستترجم نهايات الأسطر "‎\n" إلى "‎\r\n" عند العمل مع الملف، نضيف المحرف b بعد النمط إذا لم يكن هذا مقصودًا خاصةً إذا كان الملف ثنائيًا.

يجب أن يغلق تطبيق PHP أي مجرى بعد الانتهاء من استخدامه باستخدام الدالة fclose وذلك منعًا لظهور الخطأ Too many open files، يعدّ هذا مهمًا خاصةً في برامج واجهة سطر الأوامر (CLI) بما أنّ المجاري تُغلق عند إيقاف التشغيل فقط، وهذا يعني أن ذلك ليس ضروريًا جدًا في خوادم الويب (لكن يجب استخدامه أيضًا كممارسة لمنع تسرب المورد) إذا لم تتوقع تنفيذ العملية لوقتٍ طويل ولن تفتح عدة مجاري.

القراءة

نستخدم الدالة fread لقراءة عدد مُعطى من البايتات بدءًا من مؤشر الملف أو حتى الوصول إلى نهاية الملف (EOF).

قراءة أسطر

نستخدم الدالة fgets لقراءة ملف وتستمر بالقراءة حتى تصل إلى نهاية السطر (EOL) أو حتى تنتهي قراءة العدد المُعطى.

ستحرِّك كل من الدالتين fread وfgets مؤشر الملف أثناء القراءة.

قراءة كل البايتات المتبقية

تضع الدالة stream_get_contents كل البايتات المتبقية في المجرى في سلسلة نصية وتعيدها.

تعديل موضع مؤشر ملف

يكون مؤشر الملف عند فتح المجرى افتراضيًا في بداية الملف (أو في نهايته إذا اُستعمل النمط)، نستخدم الدالة fseek لنحرّك مؤشر الملف إلى موضع جديد، نسبةً لإحدى القيم الثلاثة:

  • SEEK_SET: هذه هي القيمة الافتراضية، تكون إزاحة موقع مؤشر الملف نسبةً لبدايته.
  • SEEK_CUR: إزاحة موقع مؤشر الملف نسبةً للموقع الحالي.
  • SEEK_END: إزاحة موقع مؤشر الملف نسبةً إلى نهاية الملف، ومن الشائع تمرير إزاحة سالبة لهذه القيمة، ستحرّك موقع مؤشر الملف عددًا محددًا من البايتات قبل نهاية الملف.

تعد الدالة rewind اختصارًا ملائمًا للشيفرة fseek($fh, 0, SEEK_SET)‎.

يُظهر استخدام الدالة ftell الموقع المطلق لمؤشر الملف.

يتخطى السكربت التالي أول 10 بايتات، ويقرأ 10 بايتات التالية ثم يتخطى 10 بايتات ويقرأ 10 بايتات ثم يقرأ 10 بايتات الأخيرة من الملف file.txt:

$fh = fopen("file.txt", "rb");

// ‫البدء من الإزاحة 10
fseek($fh, 10);

// قراءة 10 بايتات
echo fread($fh, 10);

// تخطي 10 بايتات
fseek($fh, 10, SEEK_CUR);

// قراءة 10 بايتات
echo fread($fh, 10);

// تخطي 10 بايتات قبل نهاية الملف
fseek($fh, -10, SEEK_END);

// قراءة 10 بايتات
echo fread($fh, 10);
fclose($fh);

الكتابة

نستخدم الدالة fwrite لكتابة سلسلة في ملف بدءًا من موقع المؤشر الحالي.

fwrite($fh, "Some text here\n");

نقل ونسخ ملفات ومجلدات

نسخ ملفات

تنسخ الدالة copy الملف المصدر المحدد في الوسيط الأول إلى الهدف المحدد في الوسيط الثاني، يجب أن يكون الهدف في مجلد مُنشأ بالفعل.

if (copy('test.txt', 'dest.txt')) {
    echo 'File has been copied successfully';
} else {
    echo 'Failed to copy file to destination given.'
}

نسخ مجلدات مع عودية (recursion)

يشبه نسخ الملفات حذفها إلى درجة كبيرة، باستثناء أننا نستخدم الدالة copy بدلًا من unlink للملفات وmkdir بدلًا من rmdir للمجلدات في بداية بدلًا من كونها في نهاية الدالة.

function recurse_delete_dir(string $src, string $dest) : int {
    $count = 0;

    // ‫التأكد من أنّ كل من ‎$src و‎$dest ينتهي بخط مائل حتى يمكننا دمجه مع أسماء الملفات
    $src = rtrim($dest, "/\\") . "/";
    $dest = rtrim($dest, "/\\") . "/";

    // ‫استخدام dir()‎ للحصول على قائمة الملفات
    $list = dir($src);

    // ‫إنشاء المجلد ‎$dest إذا لم يكن موجودًا
    @mkdir($dest);

    // ‫تخزين اسم الملف التالي في ‎$file، إذا أصبحت قيمة ‎$file هي false تُنهى الحلقة
    while(($file = $list->read()) !== false) {
        if($file === "." || $file === "..") continue;
        if(is_file($src . $file)) {
            copy($src . $file, $dest . $file);
            $count++;
        } elseif(is_dir($src . $file)) {
            $count += recurse_copy_dir($src . $file, $dest . $file);
        }
    }
    return $count;
}

إعادة التسمية/النقل

عملية إعادة التسمية/النقل أبسط بكثير، إذ يمكن نقل أو إعادة تسمية كامل المجلد في استدعاء واحد للدالة rename، أمثلة:

rename("~/file.txt", "~/file.html");
rename("~/dir", "~/old_dir");
rename("~/dir/file.txt", "~/dir2/file.txt");

تقليل استخدام الذاكرة عند التعامل مع ملفات كبيرة

يمكننا استخدام إحدى الدالتين file أو file_get_contents إذا احتجنا إلى تحليل ملف كبير مثل ملف CSV يتضمن ملايين الأسطر وحجمه أكبر من 10 ميجابايت وينتهي الأمر بالحصول على الخطأ:

Allowed memory size of XXXXX bytes exhausted

بفرض أنّ المصدر التالي top-1m.csv يحتوي على مليون سطر وحجمه حوالي 22 ميجابايت.

var_dump(memory_get_usage(true));
$arr = file('top-1m.csv');
var_dump(memory_get_usage(true));

الخرج:

int(262144)
int(210501632)

بما أنّ المفسّر يحتاج إلى حمل الأسطر في مصفوفة ‎$arr لذا فإنّه يستهلك 200 ميجابايت من ذاكرة الوصول العشوائي (RAM)، لاحظ أنّه لا يمكننا فعل أي شيء مع محتويات المصفوفة.

بفرض أنّه لدينا الشيفرة التالية:

var_dump(memory_get_usage(true));
$index = 1;

if (($handle = fopen("top-1m.csv", "r")) !== FALSE) {
    while (($row = fgetcsv($handle, 1000, ",")) !== FALSE) {
        file_put_contents('top-1m-reversed.csv',$index . ',' . strrev($row[1]) .     PHP_EOL,FILE_APPEND);
        $index++;
    }
    fclose($handle);
}
var_dump(memory_get_usage(true));

خرج الشيفرة السابقة:

int(262144)
int(262144)

لا نستخدم بايتًا واحدًا إضافيًا من الذاكرة لكن نحلل كامل ملف CSV ونحفظه إلى ملف آخر مع عكس قيمة العمود الثاني، وذلك لأنّ الدالة fgetcsv تقرأ سطر واحد فقط ويُكتب فوق ‎$row عند كل تكرار.

المجاري (streams)

اسم المعامل الوصف
Stream Resource ‫ يتألف مزود البيانات من الصيغة `‎://‎`

تسجيل مغلِّف مجرى

يوفر مغلِّف المجرى مقبضًا لمخطط محدد أو أكثر، يُظهر المثال التالي مُغلِّف مجرى بسيط يرسل طلبات PATCH HTTP عند إغلاق المجرى.

// ‫تسجيل الصنف FooWrapper مغلِّفًا لمجرى روابط ‎foo://‎
stream_wrapper_register("foo", FooWrapper::class, STREAM_IS_URL) or die("Duplicate stream wrapper
registered");

class FooWrapper {
    // ‫‫سيُعدَّل هذا من PHP لإظهار السياق الممرَّر في الاستدعاء الحالي
    public $context;

    // يستخدم هذا داخليًا في هذا المثال لتخزين الرابط
    private $url;

    // ‫عند استدعاء fopen()‎ مع بروتوكول لهذا المغلِّف، يمكن تنفيذ هذا التابع لتخزين بيانات مثل المضيف.
    public function stream_open(string $path, string $mode, int $options, string &$openedPath) :bool {
        $url = parse_url($path);
        if($url === false) return false;
            $this->url = $url["host"] . "/" . $url["path"];
            return true;
    }

    // ‫معالجة استدعاءات الدالة fwrite()‎ على هذا المجرى        
    public function stream_write(string $data) : int {
        $this->buffer .= $data;
        return strlen($data);
    }

    // ‫معالجة استدعاءات الدالة fclose()‎ على هذا المجرى
    public function stream_close() {
        $curl = curl_init("http://" . $this->url);
        curl_setopt($curl, CURLOPT_POSTFIELDS, $this->buffer);
        curl_setopt($curl, CURLOPT_CUSTOMREQUEST, "PATCH");
        curl_exec($curl);
        curl_close($curl);
        $this->buffer = "";
    }

    // ‫مُعالج استثناء قيمة تراجعية في حال القيام بعملية غير مدعومة
    // هذا ليس ضروريًا‎
    public function __call($name, $args) {
        throw new \RuntimeException("This wrapper does not support $name");
    }

    // ‫تُستدعى الشيفرة التالية عند استدعاء unlink("foo://something-else")‎
    public function unlink(string $path) {
        $url = parse_url($path);
        $curl = curl_init("http://" . $url["host"] . "/" . $url["path"]);
        curl_setopt($curl, CURLOPT_CUSTOMREQUEST, "DELETE");
        curl_exec($curl);
        curl_close($curl);
    }
}

يُظهر هذا المثال بعض الأمثلة على ما قد يحتويه مُغلِّف مجرى عام، ليست هذه كل التوابع المتوفرة، يمكنك أن تجد هنا كل التوابع المتوفرة والتي يمكن تنفيذها.

ترميز UTF-8

الدخل

يجب التحقق من كل سلسلة نصية مُستقبَلة أنّها مكتوبة بترميز UTF-8 صحيح قبل محاولة تخزينها في أي مكان، تقوم دالة mbcheckencoding() بهذا العمل لكن يجب أن تستخدمها بشكلٍ منتظم، لا توجد طريقة لحل هذه المشكلة إذ يمكن للعملاء الضارّين إرسال البيانات بأي تشفير يريدونه.

$string = $_REQUEST['user_comment'];
if (!mb_check_encoding($string, 'UTF-8')) {
    // ‫ترميز السلسلة النصية ليس UTF-8 لذا أعد تشفيرها
    $actualEncoding = mb_detect_encoding($string);
    $string = mb_convert_encoding($string, 'UTF-8', $actualEncoding);
}

يمكنك تجاهل النقطة الأخيرة إذا كنت تستخدم HTML5، إذ أنّ الطريقة الموثوقة الوحيدة لتكون كل البيانات المرسلة إليك عبر المتصفحات بالترميز UTF-8 هي إضافة السمة accept-charset لكل وسوم <form> كالتالي:

<form action="somepage.php" accept-charset="UTF-8">

الخرج

إذا كان تطبيقك ينقل نصًا إلى الأنظمة الأخرى فأنت تحتاج إلى التأكد من ترميز المحارف، يمكنك في PHP استخدام خيار default_charset في php.ini أو التصريح عن نوع المحتوى يدويًا Content-Type MIME في الترويسة وهذه الطريقة الأفضل عند استهداف المتصفحات الحديثة.

header('Content-Type: text/html; charset=utf-8');

يمكنك ضبط الترميز في ملف HTML باستخدام البيانات الوصفية إذا كنت غير قادرٍ على ضبط ترويسة الرد.

// HTML5
<meta charset="utf-8">

// ‫الإصدارات الأقدم من HTML‫
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

تخزين البيانات والوصول إليها

تتحدث هذه الفقرة بشكلٍ أساسي عن الترميز UTF-8 ودوافع استخدامه مع قاعدة البيانات.

تخزين البيانات في قاعدة بيانات MySQL: عندما تحدد مجموعة المحارف (character set) ‏utf8mb4 لكل الجداول والأعمدة النصية في قاعدة البيانات فأنت تجعل MySQL تخزّن وتستعيد القيم المرمّزة محليًا بالترميز UTF-8.

تستخدم MySQL ضمنيًا الترميز utf8mb4 إذا حُدِّد الترتيب utf8mb4_*‎ (دون أي مجموعة محارف صريحة).

لا تدعم النسخ القديمة من MySQL (الإصدار ما قبل ‏‎5.5.3‏) الترميز utf8mb4 لذا ستضطر إلى استخدام الترميز utf8 الذي يدعم مجموعة فرعية من محارف يونيكود (Unicode).

الوصول إلى البيانات في قاعدة بيانات MySQL: في شيفرة التطبيق (PHP مثلًا)، مهما كانت الطريقة التي تستخدمها للوصول إلى قاعدة البيانات فأنت تحتاج إلى ضبط ترميز محارف الاتصال لتكون utf8mb4، بهذه الطريقة لا تحوّل MySQL من ترميز UTF-8 الأصلي عند تسليم البيانات إلى تطبيقك وبالعكس.

توفر بعض برامج التشغيل آلية خاصة بها لضبط ترميز محارف الاتصال والتي تحدّث حالتها الداخلية وتُخبر MySQL بالترميز الذي سيُستخدم في الاتصال، وهذه هي الطريقة المفضّلة عادةً.

مثلًا (نطبق نفس الدوافع التي في الأعلى فيما يتعلق باستخدام ‎utf8mb4/utf8‎):

  • يمكنك تحديد ترميز المحارف باستخدام DSN إذا كنت تستخدم طبقة التجريد PDO ᠎مع إصدار PHP ≥ 5.3.6:
$handle = new PDO('mysql:charset=utf8mb4');
$conn = mysqli_connect('localhost', 'my_user', 'my_password', 'my_db');

// نمط كائني التوجه
$conn->set_charset('utf8mb4'); 

// نمط إجرائي
mysqli_set_charset($conn, 'utf8mb4');
  • يمكنك استدعاء الدالة mysql_set_charset إذا كنت تستخدم MySQL البسيطة وإصدار PHP ≥ 5.2.3.
$conn = mysql_connect('localhost', 'my_user', 'my_password');

// نمط كائني التوجه
$conn->set_charset('utf8mb4');

// نمط إجرائي
mysql_set_charset($conn, 'utf8mb4');
  • إذا لم يوفر نظام تشغيل قاعدة البيانات آلية خاصة به لضبط ترميز محارف الاتصال، قد تضطر إلى إصدار استعلام لإخبار MySQL عن الطريقة التي يتوقع بها تطبيقك تشفير البيانات عند الاتصال بالشكل: SET NAMES 'utf8mb4'‎.

دعم يونيكود في PHP

تحويل محارف يونيكود إلى تنسيق "‎\uxxxx" باستخدام PHP

يمكنك استخدام الشيفرة التالية للترميز وفك الترميز:

if (!function_exists('codepoint_encode')) {
    function codepoint_encode($str) {
        return substr(json_encode($str), 1, -1);
    }
}

if (!function_exists('codepoint_decode')) {
    function codepoint_decode($str) {
        return json_decode(sprintf('"%s"', $str));
    }
}

طريقة الاستخدام

echo "\\nUse JSON encoding / decoding\\n";
var_dump(codepoint_encode("我好"));
var_dump(codepoint_decode('\\u6211\\u597d'));

الخرج:

Use JSON encoding / decoding
string(12) "\\u6211\\u597d"
string(6) "我好"

تحويل محارف يونيكود إلى قيمها الرقمية و/أو كيانات HTML باستخدام PHP

يمكنك استخدام الشيفرة التالية للترميز وفك الترميز:

if (!function_exists('mb_internal_encoding')) {
    function mb_internal_encoding($encoding = NULL) {
        return ($from_encoding === NULL) ? iconv_get_encoding() : iconv_set_encoding($encoding);
    }
}

if (!function_exists('mb_convert_encoding')) {
    function mb_convert_encoding($str, $to_encoding, $from_encoding = NULL) {
        return iconv(($from_encoding === NULL) ? mb_internal_encoding() : $from_encoding,$to_encoding, $str);
    }
}

if (!function_exists('mb_chr')) {
    function mb_chr($ord, $encoding = 'UTF-8') {
        if ($encoding === 'UCS-4BE') {
            return pack("N", $ord);
        } else {
            return mb_convert_encoding(mb_chr($ord, 'UCS-4BE'), $encoding, 'UCS-4BE');
        }
    }
}

if (!function_exists('mb_ord')) {
    function mb_ord($char, $encoding = 'UTF-8') {
        if ($encoding === 'UCS-4BE') {
            list(, $ord) = (strlen($char) === 4) ? @unpack('N', $char) : @unpack('n', $char);
            return $ord;
        } else {
            return mb_ord(mb_convert_encoding($char, 'UCS-4BE', $encoding), 'UCS-4BE');
        }
    }
}

if (!function_exists('mb_htmlentities')) {
    function mb_htmlentities($string, $hex = true, $encoding = 'UTF-8') {
        return preg_replace_callback('/[\x{80}-\x{10FFFF}]/u', function ($match) use ($hex) {
            return sprintf($hex ? '&#x%X;' : '&#%d;', mb_ord($match[0]));
        }, $string);
    }
}

if (!function_exists('mb_html_entity_decode')) {
    function mb_html_entity_decode($string, $flags = null, $encoding = 'UTF-8') {
        return html_entity_decode($string, ($flags === NULL) ? ENT_COMPAT | ENT_HTML401 : $flags,$encoding);
    }
}

طريقة الاستخدام

echo "Get string from numeric DEC value\n";
var_dump(mb_chr(50319, 'UCS-4BE'));
var_dump(mb_chr(271));

echo "\nGet string from numeric HEX value\n";
var_dump(mb_chr(0xC48F, 'UCS-4BE'));
var_dump(mb_chr(0x010F));

echo "\nGet numeric value of character as DEC string\n";
var_dump(mb_ord('ď', 'UCS-4BE'));
var_dump(mb_ord('ď'));

echo "\nGet numeric value of character as HEX string\n";
var_dump(dechex(mb_ord('ď', 'UCS-4BE')));
var_dump(dechex(mb_ord('ď')));

echo "\nEncode / decode to DEC based HTML entities\n";
var_dump(mb_htmlentities('tchüß', false));
var_dump(mb_html_entity_decode('tchüß'));

echo "\nEncode / decode to HEX based HTML entities\n";
var_dump(mb_htmlentities('tchüß'));
var_dump(mb_html_entity_decode('tchüß'));

الخرج:

Get string from numeric DEC value
string(4) "ď"
string(2) "ď"

Get string from numeric HEX value
string(4) "ď"
string(2) "ď"

Get numeric value of character as DEC int
int(50319)
int(271)

Get numeric value of character as HEX string
string(4) "c48f"
string(3) "10f"

Encode / decode to DEC based HTML entities
string(15) "tch&#252;&#223;"
string(7) "tchüß"

Encode / decode to HEX based HTML entities
string(15) "tch&#xFC;&#xDF;"
string(7) "tchüß"

استخدام الإضافة Intl لدعم يونيكود

تُربط دوال السلسلة النصية الأصلية إلى دوال البايت المفرد ولا تعمل بشكلٍ جيد مع ترميز يونيكود (unicode)، توفر الإضافات iconv وmbstring بعض الدعم لترميز يونيكود بينما توفر الإضافة Intl الدعم الكامل. تعدّ الإضافة Intl مغلّفة للمعيار المعتمد لمكتبة ICU (‏International Components for unicode)، يمكنك الاطلاع على الموقع لمعرفة المزيد من المعلومات التفصيلية غير الموجودة هنا. اطّلع على تنفيذ بديل للإضافة Intl من بيئة العمل Symfony إذا لم تتمكن من تثبيت الإضافة.

توفر ICU تدويلًا كاملًا وترميز يونيكود هو جزء صغير منه، يمكنك التحويل بسهولة:

// إزالة البايتات السيئة للحماية من الهجمات
\UConverter::transcode($sString, 'UTF-8', 'UTF-8');

لكن يجب ألا تتخلى عن الإضافة iconv:

\iconv('UTF-8', 'ASCII//TRANSLIT', "Cliënt"); 
// "Client"

ترجمة -وبتصرف- للفصول [File handling - Streams - UTF-8 - Unicode Support in PHP] من كتاب PHP Notes for Professionals book



1 شخص أعجب بهذا


تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن