PHP理論知識之沐浴更衣重看PHP基礎(二)

接上篇,咱們繼續講解PHP基礎

PHP理論知識之沐浴更衣重看PHP基礎(二)

八、標準

PHP組件和框架的數量很多,隨之產生的問題就是:單獨開發的框架沒有考慮到與其他框架的通信。這樣對開發者和框架本身都是不利的。

打破舊局面的PHP-FIG

多位PHP框架的開發者認識到了這個問題,在2009年的 php|tek(一個受歡迎的PHP會議)上談論了這個問題。經過討論後得出:我們需要一個標準,用來提高框架的互操作性。於是這幾位在php|tek意外碰頭的PHP框架開發者組織了PHP Framework Interop Group,簡稱PHP-FIG。

PHP-FIG是框架代表自發組織的,其成員不是選舉產生的,任何人都可以申請加入PHP-FIG,並且能對處於提議階段的推薦規範提交反饋。另外,PHP-FIG發佈的是推薦規範,而不是強制規定。

PSR是什麼?

PSR是PHP Standards Recommendation(PHP推薦標準)的簡稱。截至今日,PHP-FIG發佈了五個推薦規範:

  • PSR-1:基本的代碼風格
  • PSR-2:嚴格的代碼風格
  • PSR-3:日誌記錄器接口
  • PSR-4:自動加載

你會發現只有四個,沒錯,因為第一份推薦規範PSR-0廢棄了,新發布的PSR-4替代了。

PSR-1:基本的代碼風格

如果想編寫符合社區標準的PHP代碼,首先要遵守PSR-1。遵守這個標準非常簡單,可能你已經再使用了。標準的細節就不寫啦,點鏈接就能看。

PSR-2:嚴格的代碼風格

PSR-2是在PSR-1的基礎上更進一步的定義PHP代碼規範。這個標準解決了很多世紀問題哈,比如縮進,大括號等等。細節也不多記錄啦。

另外,現在很多IDE(比如,PHPStorm)會有代碼格式化功能,設置代碼格式化的標準,編寫完代碼,然後全部格式化,可以幫助你遵循推薦規範,修復一些換行、縮進、大括號等細節。

PHP理論知識之沐浴更衣重看PHP基礎(二)

PSR-3:日誌記錄器接口

這個推薦規範與前兩個不同,這是一個接口,規定PHP日誌記錄器組件可以實現的方法。符合PSR-3推薦規範的PHP日誌記錄器組件,必須包含一個實現Psr\\Log\\LoggerInterface接口的PHP類。PSR-3接口複用了RFC 5424系統日誌協議,規定要實現的九個方法:

namespace Psr\\Log;
interface LoggerInterface
{
public function emergency($message, array $context = array());
public function alert($message, array $context = array());
public function critical($message, array $context = array());
public function error($message, array $context = array());
public function warning($message, array $context = array());
public function notice($message, array $context = array());
public function info($message, array $context = array());
public function debug($message, array $context = array());
public function log($level, $message, array $context = array());
}

每個方法對應RFC 5424協議的一個日誌級別。

使用PRS-3日誌記錄器

如果你正在編寫自己的PSR-3日誌記錄器,可以停下來了。因為已經有一些十分出色的日誌記錄器組件。比如:monolog/monolog,直接用就可以了。如果不能滿足要求,也建議在此基礎上做擴展。

PSR-4:自動加載器

這個推薦規範描述了一個標準的自動加載器策略。自動加載器策略是指,在運行時按需查找PHP類,接口或性狀,並將其載入PHP解釋器。

為什麼自動加載很重要

在PHP文件的頂部你是不是經常看到類似下面的代碼?

include 'path/to/file1.php';
include 'path/to/file2.php';
include 'path/to/file3.php';

如果只需載入幾個PHP腳本,使用這些函數(include()、include_once()、require()、require_once())能很好的完成工作。可是如果你要引入一千個PHP腳本呢?

在PSR-4推薦規範之前,PHP組件和框架的作者使用__autoload()和spl_autoload_register()函數註冊自定義的自動加載器策略。可是,每個PHP組件和框架的自動加載器都使用獨特的自動加載器。因此,使用的組件多的時候,也是很麻煩的事情。

推薦使用PSR-4自動加載器規範,就是解決這個問題,促進組件實現互操作性。

PSR-4自動加載器策略

PSR-4推薦規範不要求改變代碼的實現方式,只建議如何使用文件系統目錄結構和PHP命名空間組織代碼。PSR-4的精髓是把命名空間的前綴和文件系統中的目錄對應起來。比如,我可以告訴PHP,\\Oreilly\\ModernPHP命名空間中的類、接口和性狀在物理文件系統的src/目錄中,這樣PHP就知道,前綴為\\Oreilly\\ModernPHP的命名空間中的類、接口和性狀對應的src/目錄裡的目錄和文件。

如何編寫PSR-4自動加載器

如果你在寫自己的PSR-4自動加載器,請停下來。我們可以使用依賴管理器Composer自動生成的PSR-4自動加載器。

九、過濾HTML

使用htmlentities()函數過濾輸入。

$input = '

';
echo htmlentities($input, ENT_QUOTES, 'UTF-8');

需要注意的是:默認情況下,htmlentities()函數不會轉義單引號,而且也檢測不出輸入字符串的字符集。正確的使用方式是:

第一個參數輸入字符串;第二個參數設為ENT_QUOTES常量,轉移單引號;第三個參數設為輸入字符串的字符集

更多過濾HTML輸入的方式,可以使用HTML Purifier庫。這個庫強健且安全,缺點:慢,且可能難以配置。

十、SQL查詢

構建SQL查詢不好的方式:

$sql = sprintf(
'UPDATE users SET password = "%s" WHERE id = %s',
$_POST['password'],
$_GET['id']
);

如果 psasword=abc";-- ,則導致修改了整個users表的記錄password都未abc。如果需要在SQL查詢中使用輸入數據,要使用PDO預處理語句

十一、用戶資料信息

A.過濾用戶資料中的電子郵件地址

這裡會刪除除字符、數字和!#$%&'*+-/=?^_{|}~@.[]`之外的所有其他符號。

$email = '[email protected]';
$emailSafe = filter_var($email, FILTER_SANITIZE_EMAIL);

B.過濾用戶資料中的外國字符

$string = "外國字符";
$safeString = filter_var(
$string,
FILTER_SANITIZE_STRING,
FILTER_FLAG_STRIP_LOW|FILTER_FLAG_ENCODE_HIGH
);

十二、驗證數據

驗證數據與過濾不同,驗證不會從輸入數據中刪除信息,而是隻確認輸入數據是否符合預期。

1、驗證電子郵件地址

我們可以把某個FILTER_VALIDATE_*標誌傳給filter_var()函數,除了電子郵件地址,還可以驗證布爾值、浮點數、整數、IP地址、正則表達式和URL。

$input = '[email protected]';
$isEmail = filter_var($input, FILTER_VALIDAE_EMAIL);
if ($isEmail !== false) {
echo "Success";
} else {
echo "Fail";
}

2、密碼

哈希算法有很多種,例如:MD5、SHA1、bcrypt和scrypt。有些算法的速度很快,用於驗證數據完整性;有些算法速度則很慢,旨在提高安全性。生成密碼和存儲密碼時需要使用速度慢、安全性高的算法。

目前,經同行審查,最安全的哈希算法是bcrypt。與MD5和SHA1不同,bcrypt是故意設計的很慢。bcrypt算法會自動加鹽,防止潛在的彩虹表攻擊。bcrypt算法永不過時,如果計算機的運算速度變快了,我們只需提高工作因子的值。

重新計算密碼的哈希值

下面是登錄用戶的腳本:

session_start();
try {
// 從請求主體中獲取電子郵件地址
$email = filter_input(INPUT_POST, 'email');
// 從請求主體中獲取密碼
$password = filter_input(INPUT_POST, 'password');

// 使用電子郵件地址獲取用戶(注意,這是虛構代碼)
$user = User::findByEmail($email);
// 驗證密碼和賬戶的密碼哈希值是否匹配

if (password_verify($password, $user->password_hash) === false) {
throw new Exception('Invalid password');
}
// 如果需要,重新計算密碼的哈希值
$currentHashAlgorithm = PASSWORD_DEFAULT;
$currentHashOptions = array('cost' => 15);
$passwordNeedRehash = password_needs_rehash(
$user->password_hash,
$currentHashAlgorithm,
$currentHashOptions
);
if ($passwordNeedsRehash === true) {
// 保存新計算得出的密碼哈希值(注意,這是虛構代碼)
$user->password_hash = password_hash(
$password,
$currentHashAlgorithm,
$currentHashOptions
);
$user->save();
}
// 把登錄狀態保存到回話中
...
// 重定向到個人資料頁面
...
} catch (Exception $e) {
//異常處理
...
}

值得注意的是:在登錄前,一定要使用password_needs_rehash()函數檢查用戶記錄中現有的密碼哈希值是否過期。如果過期了,要重新計算密碼哈希值

PHP5.5.0之前的密碼哈希API

如果無法使用PHP5.5.0或以上版本,可以使用安東尼·費拉拉開發的ircmaxell/password-compat組件。這個組件實現了PHP密碼哈希API中的所有函數:

  • password_hash()
  • password_get_info()
  • password_needs_rehash()
  • password_verify()

十三、DateTime類

DateTime類提供一個面向對象接口,用於管理日期和時間。

沒有參數,創建的是一個表示當前日期和時間的實例:

$datetime = new DateTime();

傳入參數創建實例:

$datetime = new DateTime('2017-01-28 15:27');

指定格式,靜態構造:

$datetime = DateTime::createFromFormat('M j, Y H:i:s', 'Jan 2, 2017 15:27:30');

十四、DateInterval類

DateInterval實例表示長度固定的時間段(比如,“兩天”),或者相對而言的時間段(比如,“昨天”)。DateInterval實例用於修改DateTime實例。

使用DateInterval類:

// 創建DateTime實例
$datetime = new DateTime();
// 創建長度為兩週的間隔
$interval = new DateInterval('P2W');
// 修改DateTime實例
$datetime->add($interval);
echo $datetime->format('Y-m-d H:i:s');

創建反向的DateInterval實例:

// 過去一天
$interval = new DateInterval('-1 day');

十五、DateTimeZone類

如果應用要迎合國際客戶,可能要和時區鬥爭。

創建、使用時區:

$timezone = new DateTimeZone('America/New_York'); 

$datetime = new DateTime('2017-01-28', $timezone);

實例化之後,也可以使用setTimeZone()函數設置市區:

$datetime->setTimeZone(new DateTimeZone('Asia/Hong_Kong'));

十六、DatePeriod類

有時我們需要迭代處理一段時間內反覆出現的一系列日期和時間,DatePeriod類可以解決這種問題。DatePeriod類的構造方法接受三個參數,而且都必須提供:

  • 一個Datetime實例,表示迭代開始時的日期和時間。
  • 一個DateInterval實例,表示到下個日期和時間的間隔。
  • 一個整數,表示迭代的總次數。

DatePeriod實例是迭代器,每次迭代時都會產出一個DateTime實例

使用DatePeriod類:

$start = new DateTime();
$interval = new DateInterval('P2W');
$period = new DatePeriod($start, $interval, 3);
foreach ($period as $nextDateTime) {
echo $nextDateTime->format('Y-m-d H:i:s'), PHP_EOL;

}

十七、

在現代的PHP特性中,流或許是最出色但最少使用的。雖然PHP4.3.0就引入了流,但很多開發者不知道流的存在,因為很少人提及流,而且流的文檔也匱乏。官方的解釋比較難理解,一句話說就是:流的作用是在出發地和目的地之間傳輸數據

我把流理解為管道,相當於把水從一個地方引到另一個地方。在水從出發地流到目的地的過程中,我們可以過濾水,可以改變水質,可以添加水,也可以排出水(提示:水是數據的隱喻)。

流封裝協議

流式數據的種類各異,每種類型需要獨特的協議,以便讀寫數據。我們稱這些協議為流封裝協議。比如,我們可以讀寫文件系統,可以通過HTTP、HTTPS或SSH與遠程Web服務器通信,還可以打開並讀寫ZIP、RAR或PHAR壓縮文件。這些通信方式都包含下述相同的過程:

  1. 開始通信。
  2. 讀取數據。
  3. 寫入數據。
  4. 結束通信。

雖然過程一樣的,但是讀寫文件系統中文件的方式與手法HTTP消息的方式有所不同。流封裝協議的作用是使用通用的幾口封裝這些差異。

每個流都有一個協議和一個目標。格式如下:

<scheme>://<target>
/<target>/<scheme>

說這麼多有點懵,先看例子,使用HTTP流封裝協議與Flickr API通信:

$json = file_get_contents(
'http://api.flickr.com/services/feeds/photos_public.gne?format=json'
);

不要誤以為這是普通的網頁URL,file_get_contents()函數的字符串參數其實是一個流標識符。http協議會讓PHP使用HTTP流封裝協議。看起來像是普通的網頁URL,是因為HTTP流封裝協議就是這樣規定的:)。其他流封裝協議可能不是這樣。

file://流封裝協議

我們使用file_get_contents(),fopen(),fwrite()和fclose()函數讀寫文件系統。因為PHP默認使用的流封裝協議是file://,所以我們很少認為這些函數使用的是PHP流。

隱式使用file://流封裝協議:

$handle = fopen('/etc/hosts', 'rb');
while (feof($handle) !== true) {
echo fgets($handle);
}
fclose($handle);

顯式使用file://流封裝協議:

$handle = fopen('file:///etc/hosts', 'rb');
while (feof($handle) !== true) {
echo fgets($handle);
}
fclose($handle);

流上下文

有些PHP流能接受一些列可選的參數,這些參數叫流上下文,用於定製流的行為。流上下文使用stream_context_create()函數創建。

比如,你知道可以使用file_get_contents()函數發送HTTP POST請求嗎?如果想這麼做,可以使用一個流上下文對象:

$requestBody = '{"username": "beck"}';
$context = stream_context_create(array(

'http' => array(
'method' => 'POST',
'header' => "Content-Type: application/json;charset=utf-8;\\r\\n" .
"Content-Length: " . mb_strlen($requestBody),
"content" => $requestBody
)
));
$response = file_get_contents('https://my-api.com/users', false, $context);

流過濾器

關於PHP的流,其實真正強大的地方在於過濾、轉換、添加或刪除流中傳輸的數據

注意:PHP內置了幾個流過濾器:string.rot13、string.toupper、string.tolower和string.strp_tags。這些過濾器沒什麼用,我們要使用自定義的過濾器。

若想把過濾器附加到現有的流上,要使用stream_filter_append()函數。比如,想要把文件中的內容轉換成大寫字母,可以使用string.toupper過濾器。書中不建議使用這個過濾器,這裡只是演示如何把過濾器附加到流上:

$handle = fopen('data.txt', 'rb');
stream_filter_append($handle, 'string.toupper');
while (feof($handle) !== true) {
echo fgets($handle); // }
fclose($handle);

使用php://filter流封裝協議把過濾器附加到流上:

$handle = fopen('php://filter/read=string.toupper/resource=data.txt', 'rb');
while (feof($handle) !== true) {
echo fgets($handle); // }
fclose($handle);

來看個更實際的流過濾器示例,假如我們nginx訪問日誌保存在rsync.net,一天的訪問情況保存在一個日誌文件中,而且會使用bzip2壓縮每個日誌文件,名稱格式為:YYYY-MM-DD.log.bz2。某天,領導讓我提取過去30天某個域名的訪問數據。使用DateTime類和流過濾器迭代bzip壓縮的日誌文件

$dateStart = new \\DateTime();
$dateInterval = \\DateInterval::createFromDateString('-1 day');
$datePeriod = new \\DatePeriod($dateStart, $dateInterval, 30);//創建迭代器
foreach ($datePeriod as $date) {
$file = 'sftp://USER:[email protected]/' . $date->format('Y-m-d') . 'log.bz2';
if (file_exists($file)) {
$handle = fopen($file, 'rb');
stream_filter_append($handle, 'bzip2.decompress');
while (feof($handle) !== true) {
$line = fgets($handle);
if (strpos($line, 'www.example.com') !== false) {
fwrite(STDOUT, $line);
}
}
fclose($handle);
}
}

計算日期範圍,確定日誌文件的名稱,通過FTP連接rsync.net,下載文件,解壓縮文件,逐行迭代每個文件,把相應的行提取出來,然後把訪問數據寫入一個輸出目標。使用PHP流,不到20行代碼就能做完所有這些事情。

自定義流過濾器

其實大多數情況下都要使用自定義的流過濾器。自定義的流過濾器是個PHP類,繼承內置的php_user_filter類。這個類必須實現filter()、onCreate()和onClose()方法。而且,必須使用stream_filter_register()函數註冊自定義的流過濾器。

PHP流會把數據分成按次序排列的桶,一個桶中盛放的流數據量是固定的。一定時間內過濾器接收到的桶叫做桶隊列。桶隊列中的每個桶對象都有兩個公開屬性:data和datalen,分別是桶中的內容和內容的長度。

下面定義一個處理髒字的流過濾器

class DirtyWordsFilter extends php_user_filter
{
/**
* @param resource $in 流來的桶隊列
* @param resource $out 流走的桶隊列
* @param resource $consumed 處理的字節數
* @param resource $closing 是流中最後一個桶隊列嗎?
*/
public function filter()
{
$words = array('grime', 'dirt', 'grease');
$wordData = array();
foreach ($words as $word) {
$replacement = array_fill(0, mb_strlen($word), '*');

$wordData[$word] = implode(' ', $replacement);
}
$bad = array_keys($wordData);
$goods = array_values($wordData);
// 迭代流來的桶隊列中的每個桶
while ($bucket = stream_bucket_make_writeable($in)) {
// 審查桶數據中的髒字
$bucket->data = str_replace($bad, $goods, $bucket->data);
// 增加已處理的數據量
$consumed += $bucket->datalen;
// 把桶放入流向下游的隊列中
stream_bucket_append($out, $bucket);
}
return PSFS_PASS_ON;
}
}

filter()方法的作用是接受、處理再轉運桶中的流數據。這個方法的返回值是PSFS_PASS_ON常量,表示操作成功。

註冊流過濾器

接著,我們必須使用stream_filter_register()函數註冊這個自定義的DirtWordsFilter流過濾器:

stream_filter_register('dirty_words_filter', 'DirtWordsFilter');

第一個參數是用於識別這個自定義過濾器的過濾器名,第二個參數是自定義過濾器的類名。

使用DirtWordsFilter流過濾器

$handle = fopen('data.txt', 'rb');
stream_filter_append($handle, 'dirty_words_filter');
while (feof($handle) !== true) {
echo fgets($handle); // }
fclose($handle);

十八、錯誤與異常

對錯誤和異常的處理,一定要遵守四個規則:

  • 一定要讓PHP報告錯誤。
  • 在開發環境中要顯示錯誤。
  • 在生產環境中不能顯示錯誤。
  • 在開發環境和生產環境中都要記錄錯誤。


分享到:


相關文章: