框架提供了快速應用程式開發的工具,但通常會隨著您創建功能的速度而增加技術債務。當可維護性不是開發人員有目的地關注的焦點時,就會產生技術債。由於缺乏單元測試和結構,未來的變更和調試成本高昂。
以下是如何開始建立程式碼以實現可測試性和可維護性 - 並節省您的時間。
#我們將(鬆散地)覆蓋
- 乾燥
- 依賴注入
- 介面
- 容器
- 使用 PHPUnit 進行單元測試
讓我們從一些人為但典型的程式碼開始。這可能是任何給定框架中的模型類別。
class User { public function getCurrentUser() { $user_id = $_SESSION['user_id']; $user = App::db->select('id, username') ->where('id', $user_id) ->limit(1) ->get(); if ( $user->num_results() > 0 ) { return $user->row(); } return false; } }
此程式碼可以工作,但需要改進:
- 這是不可測試的。
- 我們依賴
$_SESSION
全域變數。單元測試框架(例如 PHPUnit)依賴命令列,其中$_SESSION
和許多其他全域變數不可用。 - 我們依賴資料庫連線。理想情況下,在單元測試中應避免實際的資料庫連接。測試是關於程式碼,而不是數據。
- 我們依賴
- 此程式碼的可維護性並不理想。例如,如果我們更改資料來源,則需要更改應用程式中使用的每個
App::db
實例中的資料庫程式碼。另外,如果我們不想要目前使用者的資訊怎麼辦?
嘗試的單元測試
這裡嘗試為上述功能建立單元測試。
class UserModelTest extends PHPUnit_Framework_TestCase { public function testGetUser() { $user = new User(); $currentUser = $user->getCurrentUser(); $this->assertEquals(1, $currentUser->id); } }
讓我們檢查一下。首先,測試將會失敗。 User
物件中使用的 $_SESSION
變數在單元測試中不存在,因為它在命令列中執行 PHP。
其次,沒有資料庫連線設定。這意味著,為了完成這項工作,我們需要引導我們的應用程式以獲得 App
物件及其 db
物件。我們還需要一個可用的資料庫連線來進行測試。
為了讓這個單元測試運作,我們需要:
- 為我們的應用程式中執行的 CLI (PHPUnit) 設定配置
- 依賴資料庫連線。這樣做意味著依賴與我們的單元測試分開的資料來源。如果我們的測試資料庫沒有我們期望的資料怎麼辦?如果我們的資料庫連線很慢怎麼辦?
- 依賴引導的應用程式會增加測試的開銷,從而顯著減慢單元測試的速度。理想情況下,我們的大多數程式碼都可以獨立於所使用的框架進行測試。
那麼,讓我們開始討論如何改進這一點。
保持程式碼乾燥
在這個簡單的上下文中,檢索目前使用者的函數是不必要的。這是一個人為的範例,但本著 DRY 原則的精神,我選擇進行的第一個最佳化是概括此方法。
class User { public function getUser($user_id) { $user = App::db->select('user') ->where('id', $user_id) ->limit(1) ->get(); if ( $user->num_results() > 0 ) { return $user->row(); } return false; } }
這提供了一種我們可以在整個應用程式中使用的方法。我們可以在呼叫時傳入當前用戶,而不是將該功能傳遞給模型。當程式碼不依賴其他功能(例如會話全域變數)時,程式碼會更加模組化和可維護。
但是,這仍然無法按預期進行測試和維護。我們仍然依賴資料庫連線。
依賴注入
讓我們透過加入一些依賴注入來幫助改善這種情況。當我們將資料庫連接傳遞到類別時,我們的模型可能如下所示。
class User { protected $_db; public function __construct($db_connection) { $this->_db = $db_connection; } public function getUser($user_id) { $user = $this->_db->select('user') ->where('id', $user_id) ->limit(1) ->get(); if ( $user->num_results() > 0 ) { return $user->row(); } return false; } }
現在,我們的 User
模型的依賴項已經提供了。我們的類別不再假定某個資料庫連接,也不再依賴任何全域物件。
至此,我們的類別就基本上可以測試了。我們可以傳入我們選擇的資料來源(大部分)和使用者 ID,並測試該呼叫的結果。我們還可以切換單獨的資料庫連接(假設兩者都實現相同的檢索資料的方法)。酷。
讓我們看看單元測試可能會是什麼樣子。
<?php use Mockery as m; use Fideloper\User; class SecondUserTest extends PHPUnit_Framework_TestCase { public function testGetCurrentUserMock() { $db_connection = $this->_mockDb(); $user = new User( $db_connection ); $result = $user->getUser( 1 ); $expected = new StdClass(); $expected->id = 1; $expected->username = 'fideloper'; $this->assertEquals( $result->id, $expected->id, 'User ID set correctly' ); $this->assertEquals( $result->username, $expected->username, 'Username set correctly' ); } protected function _mockDb() { // "Mock" (stub) database row result object $returnResult = new StdClass(); $returnResult->id = 1; $returnResult->username = 'fideloper'; // Mock database result object $result = m::mock('DbResult'); $result->shouldReceive('num_results')->once()->andReturn( 1 ); $result->shouldReceive('row')->once()->andReturn( $returnResult ); // Mock database connection object $db = m::mock('DbConnection'); $db->shouldReceive('select')->once()->andReturn( $db ); $db->shouldReceive('where')->once()->andReturn( $db ); $db->shouldReceive('limit')->once()->andReturn( $db ); $db->shouldReceive('get')->once()->andReturn( $result ); return $db; } }
我在此單元測試中添加了一些新內容:Mockery。 Mockery 允許您「模擬」(偽造)PHP 物件。在本例中,我們正在模擬資料庫連接。透過我們的模擬,我們可以跳過測試資料庫連接並簡單地測試我們的模型。
想要了解更多關於 Mockery 的資訊嗎?
在本例中,我們正在模擬 SQL 連線。我們告訴模擬物件期望呼叫 select
、where
、limit
和 get
# 方法。我返回 Mock 本身,以反映 SQL 連接物件如何返回自身 ($this
),從而使其方法呼叫「可連結」。請注意,對於 get
方法,我傳回資料庫呼叫結果 - 填入了使用者資料的 stdClass
物件。
這解決了一些問題:
- 我们仅测试我们的模型类。我们还没有测试数据库连接。
- 我们能够控制模拟数据库连接的输入和输出,因此可以可靠地测试数据库调用的结果。我知道由于模拟数据库调用,我将获得用户 ID“1”。
- 我们不需要引导我们的应用程序,也不需要提供任何配置或数据库来进行测试。
我们还可以做得更好。这就是它变得有趣的地方。
接口
为了进一步改进这一点,我们可以定义并实现一个接口。考虑以下代码。
interface UserRepositoryInterface { public function getUser($user_id); } class MysqlUserRepository implements UserRepositoryInterface { protected $_db; public function __construct($db_conn) { $this->_db = $db_conn; } public function getUser($user_id) { $user = $this->_db->select('user') ->where('id', $user_id) ->limit(1) ->get(); if ( $user->num_results() > 0 ) { return $user->row(); } return false; } } class User { protected $userStore; public function __construct(UserRepositoryInterface $user) { $this->userStore = $user; } public function getUser($user_id) { return $this->userStore->getUser($user_id); } }
这里发生了一些事情。
- 首先,我们为用户数据源定义一个接口。这定义了
addUser()
方法。 - 接下来,我们实现该接口。在本例中,我们创建一个 MySQL 实现。我们接受一个数据库连接对象,并使用它从数据库中获取用户。
- 最后,我们在
User
模型中强制使用实现UserInterface
的类。这样可以保证数据源始终有一个可用的getUser()
方法,无论使用哪个数据源来实现UserInterface
。
请注意,我们的
User
对象类型提示UserInterface
在其构造函数中。这意味着实现UserInterface
的类必须传递到User
对象中。这是我们所依赖的保证 - 我们需要getUser
方法始终可用。
这样做的结果是什么?
- 我们的代码现在完全可测试。对于
User
类,我们可以轻松地模拟数据源。 (测试数据源的实现将是单独的单元测试的工作)。 - 我们的代码更加易于维护。我们可以切换不同的数据源,而无需更改整个应用程序的代码。
- 我们可以创建任何数据源。 ArrayUser、MongoDbUser、CouchDbUser、MemoryUser 等
- 如果需要,我们可以轻松地将任何数据源传递到我们的
User
对象。如果您决定放弃 SQL,则只需创建一个不同的实现(例如MongoDbUser
)并将其传递到您的User
模型中。
我们还简化了单元测试!
<?php use Mockery as m; use Fideloper\User; class ThirdUserTest extends PHPUnit_Framework_TestCase { public function testGetCurrentUserMock() { $userRepo = $this->_mockUserRepo(); $user = new User( $userRepo ); $result = $user->getUser( 1 ); $expected = new StdClass(); $expected->id = 1; $expected->username = 'fideloper'; $this->assertEquals( $result->id, $expected->id, 'User ID set correctly' ); $this->assertEquals( $result->username, $expected->username, 'Username set correctly' ); } protected function _mockUserRepo() { // Mock expected result $result = new StdClass(); $result->id = 1; $result->username = 'fideloper'; // Mock any user repository $userRepo = m::mock('Fideloper\Third\Repository\UserRepositoryInterface'); $userRepo->shouldReceive('getUser')->once()->andReturn( $result ); return $userRepo; } }
我们已经完全取消了模拟数据库连接的工作。相反,我们只是模拟数据源,并告诉它当调用 getUser
时要做什么。
但是,我们仍然可以做得更好!
容器
考虑我们当前代码的用法:
// In some controller $user = new User( new MysqlUser( App:db->getConnection("mysql") ) ); $user->id = App::session("user->id"); $currentUser = $user->getUser($user_id);
我们的最后一步是引入容器。容器。在上面的代码中,我们需要创建并使用一堆对象来获取当前用户。此代码可能散布在您的应用程序中。如果您需要从 MySQL 切换到 MongoDB,您仍然需要编辑上述代码出现的每个位置。那几乎不是干的。容器可以解决这个问题。
容器只是“包含”一个对象或功能。它类似于应用程序中的注册表。我们可以使用容器自动实例化一个新的 User
对象以及所有需要的依赖项。下面,我使用 Pimple,一个流行的容器类。
// Somewhere in a configuration file $container = new Pimple(); $container["user"] = function() { return new User( new MysqlUser( App:db->getConnection('mysql') ) ); } // Now, in all of our controllers, we can simply write: $currentUser = $container['user']->getUser( App::session('user_id') );
我已将 User
模型的创建移至应用程序配置中的一个位置。结果是:
- 我们的代码保持干燥。
User
对象和选择的数据存储在我们应用程序的一个位置定义。 - 我们可以将
User
模型从使用 MySQL 切换到 ONE 位置中的任何其他数据源。这更易于维护。
最终想法
在本教程的过程中,我们完成了以下任务:
- 保持我们的代码干燥且可重用
- 创建了可维护的代码 - 如果需要,我们可以在整个应用程序的一个位置切换对象的数据源
- 使我们的代码可测试 - 我们可以轻松模拟对象,而无需依赖引导我们的应用程序或创建测试数据库
- 了解如何使用依赖注入和接口来创建可测试和可维护的代码
- 了解容器如何帮助我们的应用程序更易于维护
我相信您已经注意到,我们以可维护性和可测试性的名义添加了更多代码。可以对这种实现提出强有力的论据:我们正在增加复杂性。事实上,这需要项目的主要作者和合作者对代码有更深入的了解。
但是,技术债务总体减少远远超过了解释和理解的成本。
- 代码的可维护性大大提高,可以在一个位置而不是多个位置进行更改。
- 能够(快速)进行单元测试将大幅减少代码中的错误 - 特别是在长期或社区驱动的(开源)项目中。
- 提前做额外的工作将节省时间并减少以后的麻烦。
资源
您可以使用 Composer 轻松地将 Mockery 和 PHPUnit 包含到您的应用程序中。将这些添加到 composer.json
文件中的“require-dev”部分:
"require-dev": { "mockery/mockery": "0.8.*", "phpunit/phpunit": "3.7.*" }
然后,您可以按照“dev”要求安装基于 Composer 的依赖项:
$ php composer.phar install --dev
在 Nettuts+ 上了解有关 Mockery、Composer 和 PHPUnit 的更多信息。
- 嘲笑:更好的方法
- 使用 Composer 轻松进行包管理
- 测试驱动的 PHP
对于 PHP,请考虑使用 Laravel 4,因为它特别利用了容器和此处介绍的其他概念。
感谢您的阅读!
以上是建立可測試且可維護的 PHP 程式碼的詳細內容。更多資訊請關注PHP中文網其他相關文章!

使用數據庫存儲會話的主要優勢包括持久性、可擴展性和安全性。 1.持久性:即使服務器重啟,會話數據也能保持不變。 2.可擴展性:適用於分佈式系統,確保會話數據在多服務器間同步。 3.安全性:數據庫提供加密存儲,保護敏感信息。

在PHP中實現自定義會話處理可以通過實現SessionHandlerInterface接口來完成。具體步驟包括:1)創建實現SessionHandlerInterface的類,如CustomSessionHandler;2)重寫接口中的方法(如open,close,read,write,destroy,gc)來定義會話數據的生命週期和存儲方式;3)在PHP腳本中註冊自定義會話處理器並啟動會話。這樣可以將數據存儲在MySQL、Redis等介質中,提升性能、安全性和可擴展性。

SessionID是網絡應用程序中用來跟踪用戶會話狀態的機制。 1.它是一個隨機生成的字符串,用於在用戶與服務器之間的多次交互中保持用戶的身份信息。 2.服務器生成並通過cookie或URL參數發送給客戶端,幫助在用戶的多次請求中識別和關聯這些請求。 3.生成通常使用隨機算法保證唯一性和不可預測性。 4.在實際開發中,可以使用內存數據庫如Redis來存儲session數據,提升性能和安全性。

在無狀態環境如API中管理會話可以通過使用JWT或cookies來實現。 1.JWT適合無狀態和可擴展性,但大數據時體積大。 2.Cookies更傳統且易實現,但需謹慎配置以確保安全性。

要保護應用免受與會話相關的XSS攻擊,需採取以下措施:1.設置HttpOnly和Secure標誌保護會話cookie。 2.對所有用戶輸入進行輸出編碼。 3.實施內容安全策略(CSP)限制腳本來源。通過這些策略,可以有效防護會話相關的XSS攻擊,確保用戶數據安全。

优化PHP会话性能的方法包括:1.延迟会话启动,2.使用数据库存储会话,3.压缩会话数据,4.管理会话生命周期,5.实现会话共享。这些策略能显著提升应用在高并发环境下的效率。

theSession.gc_maxlifetimesettinginphpdeterminesthelifespanofsessiondata,setInSeconds.1)它'sconfiguredinphp.iniorviaini_set().2)abalanceisesneededeededeedeedeededto toavoidperformance andunununununexpectedLogOgouts.3)

在PHP中,可以使用session_name()函數配置會話名稱。具體步驟如下:1.使用session_name()函數設置會話名稱,例如session_name("my_session")。 2.在設置會話名稱後,調用session_start()啟動會話。配置會話名稱可以避免多應用間的會話數據衝突,並增強安全性,但需注意會話名稱的唯一性、安全性、長度和設置時機。


熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

Video Face Swap
使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

MantisBT
Mantis是一個易於部署的基於Web的缺陷追蹤工具,用於幫助產品缺陷追蹤。它需要PHP、MySQL和一個Web伺服器。請查看我們的演示和託管服務。

禪工作室 13.0.1
強大的PHP整合開發環境

ZendStudio 13.5.1 Mac
強大的PHP整合開發環境

SublimeText3漢化版
中文版,非常好用

Atom編輯器mac版下載
最受歡迎的的開源編輯器