[譯] 如何創(chuàng)建你自己的 PHP 框架

平常在開發(fā)工作里胸完,重復早輪子的機會其實不很多。今天去SegmentFault論壇看到時候翘贮,翻到了以前的一個帖子赊窥,說的是如何寫自己的PHP框架。意見不一狸页。但是有幸看到了Symphony作者寫的一個系列博文:How to create your own PHP framework锨能,先動手翻譯看看(原文已經整理在Symphony官網(wǎng))。

介紹

Symphony是一個解決常見web開發(fā)問題的框架芍耘,它由一系列可復用的獨立址遇,解耦,并具有內在聯(lián)系的PHP組件構成斋竞。

與其選擇使用較為底層的組件倔约,你可以使用已經完備的全棧式web框架Symphony,或者窃页,你也可以自己造一個跺株。這個系列教程就是告訴你如何建造自己的框架复濒。

你為什么要建造自己的框架脖卖?

為什么把建造自己的框架放在第一位呢乒省?如果你看看周圍,每個人都會告訴你重復造輪子是個壞主意畦木,因為你可以選擇現(xiàn)成的袖扛,更好的框架。大多數(shù)時候十籍,他們確實是對的蛆封。但是一下幾點可以告訴你,為什么你要自己造輪子:

  • 為了學習流行web框架中更底層的知識勾栗,尤其是與Symphony框架相關的惨篱;
  • 為了滿足你特定的需求而定制框架(前提是你必須非常清楚你的需求);
  • 僅僅為了好玩而學習围俘;
  • 為了重構很久以前的框架砸讳,融入流行框架的設計思想;
  • 為了向別人炫耀你可以的界牡!

這個教程會一步一步教你如何構造框架簿寂,每一步你都會得到一個投入使用的框架,你可以用它作為自己最初的起點宿亡。慢慢的常遂,它會從一個簡單框架變?yōu)榫哂卸喾N特性的框架,最終你將獲得一個全功能的完備web框架挽荠。

如果沒有足夠的時間讀完整個教程克胳,你看一看 Slix 可以快速上手,這是一個基于Symphony的微型框架圈匆。代碼非常簡潔毯欣,考量了許多Symphony本身的組件

許多流行web框架將他們描述為MVC框架,這篇教程不會告訴你MVC設計模式臭脓,因為Symphony組件可以滿足各種設計模式酗钞,而不僅僅是MCV,當然了来累,如果你看一看MVC語義砚作,這本書會告訴你如何構造MVC當中的Controller。至于Model還有View嘹锁,這要看你個人口味葫录,而且你可以使用第三方庫來滿足需求(Doctrine,Propel 或者 plain-old PDO 來完成Model领猾;PHP 或者 Twig 來完成View)米同。

當決定構造一個框架的時候骇扇,按照MVC的設計模式來未必是一個正確的目標。最為正確的目標應該是Separation of Concerns(需求的分離)面粮,這可能是唯一一個你需要關心的設計模式少孝。Symphony的基礎概念關注點在HTTP的定義上。所以說熬苍,你將要打造的框架應該更加準確的定義為HTTP框架或者說響應/請求框架稍走。

正式開始之前

僅僅閱讀如何構造框架是不夠的。你需要自己動手嘗試教程里的每一個例子柴底。當然婿脸,你需要一個PHP環(huán)境(5.3.9或者更新),一個web服務器(比如Apache柄驻,Nginx狐树,或者PHP自建的web服務器),了解PHP基本知識以及面向對象編程鸿脓。

準備好了么抑钟,開始吧!

Bootstrapping 啟動

在你開始構思你的框架之前答憔,你需要想一想一些conventions(慣例):你的代碼將存貯子在哪里味赃?怎么命名你的class(類),怎么引用外部依賴包虐拓,等等

我們將新建一個目錄心俗,來存放你的代碼:

$ mkdir framework
$ cd framework

Dependency Management 依賴管理

為了安裝Symfony組件,你將使用Composer蓉驹,一個依賴包管理工具城榛。如果你還沒有安裝。點擊這里下載态兴。

我們的項目

這里狠持,我們沒有從0開始構建(from the scratch),我們將不斷的重寫“應用”瞻润,每一次加入一些抽象的成分喘垂。我們先從寫一個最簡單的web應用開始:

// framework/index.php
$input = $_GET['name'];
printf('Hello %s', $input);

如果你使用PHP 5.4,你可以使用PHP自建的服務器來運行這個應用绍撞,地址是http://localhost:4321/index.php?name=Fabien正勒。否則,你需要用到Apache后者Nginx其他web服務器傻铣。

$ php -S 127.0.0.1:4321

下一章章贞,我們將介紹HttpFoundation組件。

HttpFoundation 組件

在開始之前非洲,我們回過頭來想想為什么你需要一個PHP框架而不是純PHP應用(plain-old)鸭限。為什么使用框架蜕径,甚至使用最簡單的代碼片段(code snippet)是一個好主意。還有為什么創(chuàng)造一個基于Symphony組建的框架要好于從零開始搭框架败京。

我們不談論僅僅需要幾個程序員兜喻,就可以利用框架創(chuàng)造大型應用的傳統(tǒng)好處⌒希互聯(lián)網(wǎng)上已經有很多豐富的資源虹统。

盡管我們前一章寫的小應用已經足夠簡單弓坞,它仍然有很多問題:

// framework/index.php
$input = $_GET['name'];
printf('Hello %s', $input);

第一點隧甚,如果name參數(shù)沒有在URL里面定義,你會得到一個PHP warning渡冻,我們這樣解決:

// framework/index.php
$input = isset($_GET['name']) ? $_GET['name'] : 'World';
printf('Hello %s', $input);

但是戚扳,這樣的應用依然是不安全的,因為即使是這樣一個簡單的PHP代碼片段在面對世界上范圍最廣的安全威脅XSS(Cross0Site Scripting) 跨站攻擊面前族吻,也是脆弱的帽借。這里有一個更安全的版本:

$input = isset($_GET['name']) ? $_GET['name'] : 'World’;
header('Content-Type: text/html; charset=utf-8');
printf('Hello %s', htmlspecialchars($input, ENT_QUOTES, 'UTF-8'));

你可能已經注意到了,使用 htmlsepcialchars
乏味而且容易出錯(tedious and error prone)超歌。這就是為什么要使用類似Twig模板引擎的原因了砍艾。它可以默認autoescatping,使用準確的escaping要比使用一個簡單的escaping過濾要更好

正如你所見的巍举,假如我們要考慮避免PHP warning/notices 還有讓代碼更安全的話脆荷,我們所寫的代碼已經不是最簡單的了。

更進一步說懊悯,代碼甚至已經不能被簡單的測試了蜓谋。就算沒有太多可以測試的地方,針對這種最簡單的代碼片段使用單元測試是一種不自然的炭分,感覺不漂亮到方式桃焕。這里我們寫了一個試探性的PHPUnit 單元測試:

// framework/test.php
class IndexTest extends \PHPUnit_Framework_TestCase
{
    public function testHello()
    {
        $_GET['name'] = 'Fabien';
        ob_start();
        include 'index.php';
        $content = ob_get_clean();
        $this->assertEquals('Hello Fabien', $content);
}

如果我們的應用稍微復雜,我們可能會遇到更多的問題捧毛。如果你對此表示好奇观堂,可以閱讀Symphony versus Flat PHP的文檔。
如果到了這一步呀忧,你還對使用框架來構建項目不放心的話(安全和測試是使用框架最好的理由)师痕,那么你可以回去寫自己的代碼了。
當然荐虐,使用框架不僅僅是為了更好的測試和安全性七兜,更重要的是要記住使用框架可以讓開發(fā)更快速。

使用HttpFoundation組建來面向對象

寫web應用就是和HTTP協(xié)議打交道福扬。所以腕铸,框架的核心應該是圍繞HTTP的規(guī)范惜犀。
HTTP 規(guī)范描述了客戶端(比如瀏覽器)如何與服務端(web服務器)進行交互。 嚴格規(guī)范的消息(well defined message)狠裹,請求和響應虽界,構成了客戶端與服務器之間的對話:客戶端發(fā)送請求到服務器,服務器返回一個響應涛菠。

在PHP中莉御,請求通過全局變量($_GET, $_POST, $_FILE, $_COOKIE, $_SESSION)來獲得,響應通過方法(echo, header, setcookie) 來實現(xiàn)俗冻。

寫出優(yōu)美代碼的第一步就是使用面向對象的理念礁叔,即通過Symphony HttpFoundation組件來取代默認的PHP全局變量和方法。

在使用這個組件之前迄薄,我們需要添加組件的依賴:

$ composer require symfony/http-foundation

運行這個命令將自動下載Symphony HttpFoundation組件琅关,并且將他安裝在當前目錄下的vendor/目錄下。同時也產生了composer.json和composer.lock文件讥蔽,包含了如下內容:

{
    "require": {
        "symfony/http-foundation": "^2.7"
    }
}

上面的代碼展示了composer.json的內容涣易。

Class Autoloading 類的自動加載

當安裝一個新的依賴時,Composer也會自動生成一個vendor/autoloadphp
文件冶伞,讓類能夠自動加載 autoloaded新症。沒有自動加載,你需要在使用這個類之前响禽,require這個類文件徒爹。 但是由于PSR-0,我們可以使用Composer來讓PHP完成繁碎的工作金抡。

現(xiàn)在瀑焦,我們利用 Request類 和 Response類 重寫應用:

// framework/index.php
require_once __DIR__.'/vendor/autoload.php';
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

$request = Request::createFromGlobals();
$input = $request->get('name', 'World');
$response = new Response(sprintf('Hello %s', htmlspecialchars($input, ENT_QUOTES, 'UTF-8')));
$response->send();

createFromGlobals()方法創(chuàng)建了一個基于當前PHP全局變量的Request對象。

send()方法發(fā)送一個Response()對象返回客戶端(在返回內容之后梗肝,返回HTTP header)榛瓮。

在調用send()之前,我們需要再調用prepare()方法($response->prepare($request))來保證我們的響應是符合HTTP規(guī)范的巫击。例如禀晓,如果我們使用HEAD方法,這將會移除響應的內容

這里使用組件的最主要區(qū)別就是你對HTTP 消息有足夠的掌控權坝锰,你可以根據(jù)需求創(chuàng)造任意的請求和響應粹懒。

我們沒有明確設置Content-Type頭部,因為默認情況下顷级,響應的頭部就是UTF-8格式

通過Request請求類凫乖,利用簡單精巧的API,你可以獲取任意請求的消息。

// the URI being requested (e.g. /about) minus any query parameters
$request->getPathInfo();

// retrieve GET and POST variables respectively
$request->query->get('foo');
$request->request->get('bar', 'default value if bar does not exist');

// retrieve SERVER variables
$request->server->get('HTTP_HOST');

// retrieves an instance of UploadedFile identified by foo
$request->files->get('foo');

// retrieve a COOKIE value
$request->cookies->get('PHPSESSID');

// retrieve an HTTP request header, with normalized, lowercase keys
$request->headers->get('host');
$request->headers->get('content_type');

$request->getMethod();    // GET, POST, PUT, DELETE, HEAD
$request->getLanguages(); // an array of languages the client accepts

你也可以模擬一個請求:

$request = Request::create('/index.php?name=Fabian');

通過 Response 類帽芽,你可以生成一個響應(Response):

$response = new Response();
$response->setContent('Hello world!');
$response->setStatusCode(200);
$response->headers->set('Content-Type', 'text/html');

// configure the HTTP cache headers
$response->setMaxAge(10);

如果要debug一個響應删掀,把它轉化成一個string,它會返回Http協(xié)議形式的header和content.

最后导街,以上的這些Sympony當中的類披泪,他們的安全性是得到了第三方獨立公司的審查(audit)的。作為開源軟件搬瑰,Symphony的源碼接受了來自世界各地的開發(fā)者的貢獻和完善(對于潛在的安全性問題)款票。你最后一次對你創(chuàng)建的框架進行安全審查,是在什么時候泽论?
甚至簡單到獲取客戶端的ip地址都可以變得不安全:

if ($myIp == $_SERVER['REMOTE_ADDR']) {
    // the client is a known one, so give it some more privilege
}

上面的代碼已經很好了艾少,除非你在生產服務器的上一層加了逆向代理(reverse proxy)。如果是這樣佩厚,你需要編輯代碼滿足同時在開發(fā)環(huán)境(沒有代理的環(huán)境)以及遠程的生產環(huán)境的正常使用姆钉。

使用Request::getClientIp() 從一開始就會讓你好很多(它涵蓋了上面的情況):

$request = Request::createFromGlobals();
if ($myIp == $request->getClientIp()) {
    // the client is a known one, so give it some more privilege
}

同時他還有一個好處说订,它自身就很安全抄瓦。這里的意思就是說,$_SERVER[‘HTTP_X_FORWARDED_FOR’] 這個獲取得到的值是不能被信任打陶冷,因為在實際情況中钙姊,當沒有代理的時候它可以被用戶篡改。所以埂伦,如果你在生產環(huán)境中沒有使用代理煞额,它既容易被系統(tǒng)拒絕處理(因為_SERVER[‘HTTP_X_FORWARDED_FOR’] 被篡改)。如果使用 getClientIp()
就不會有這種情況沾谜,因為你需要使用之前明確使用 setTrustedProxies():

Request::setTrustedProxies(array('10.0.0.1'));
if ($myIp == $request->getClientIp(true)) {
    // the client is a known one, so give it some more privilege
}

所以膊毁,getClientIp() 方法適用于各種情況。你可以在所有的項目當中使用它基跑,不管你的服務器配置如何婚温,代碼都可以安全正確的運行。

其實這就是使用模版的好處了媳否,如果你從頭開始寫模版栅螟,你必須要考慮類似的所有情況。那你為什么不利用已經寫好的服務呢篱竭?

如果你想了解更多關于 HttpFoundation Component
, 你可以查閱 HttpFoundation 的API力图,或者閱讀完備的文檔。

到這里掺逼,我們已經寫了我們第一個框架了吃媒,如果你不想再深入下去也可以。 單單使用 Symphony HttpFoundation 組件以及讓你可以寫出更好,更易于測試的代碼了赘那。它也幫你處理了很多開發(fā)過程中遇到過的歷史問題惑朦。

事實上,類似 Drupal 的項目已經適配 HttpFoundation 組件來為他們所用漓概, 這也同樣對你適用漾月。不要重復造輪子。

我忘記告訴你了胃珍,學會使用 Symphony HttpFoundation 組件還有一個好處梁肿,由于它在目前主流框架中的流行(Sympony, Drupal 8, phpBB 4, ezPublish 5, Laravel, Silex, 還有其他),這些框架內部操作性會更好觅彰。上手會更快吩蔑。

前端控制器 The Front Controller

到目前為止,我們的應用就是簡單的單頁面填抬,我們通過新建一個頁面烛芬,讓事情變得更有趣。

// framework/bye.php
require_once __DIR__.'/vendor/autoload.php';

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
$request = Request::createFromGlobals();
$response = new Response('Goodbye!');
$response->send();

正如你所看到的飒责,大多數(shù)代碼和第一頁是一樣的赘娄。我們這里提煉出通用的代碼,這樣可以在不同的頁面間使用宏蛉。代碼的共享聽起來似乎是一個構件框架的不錯的計劃遣臼。

PHP風格的重構有點像下面的文件:

// framework/init.php
require_once __DIR__.'/vendor/autoload.php';

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

$request = Request::createFromGlobals();
$response = new Response();

實踐效果如下

// framework/index.php
require_once DIR.'/init.php';
$input = $request->get('name', 'World');
$response->setContent(sprintf('Hello %s', htmlspecialchars($input, ENT_QUOTES, 'UTF-8')));
$response->send();

GoodBye 頁面設置如下

// framework/bye.php
require_once __DIR__.'/init.php';

$response->setContent('Goodbye!');
$response->send();

我們確實需要把大部分重復性的代碼放在一個地方,但是這不是所謂的抽象拾并。我們需要每個頁面都放置一個send方法揍堰,讓頁面以模板的形式表現(xiàn)出來,可以很方便的測試代碼嗅义。

而且屏歹,新建一個新頁面意味著我們需要新的php腳本文件,文件名通過URL(http://127.0.0.1:4321/bye.php)暴露到客戶端之碗。實際上蝙眶,每一個php腳本文件都對應了一個特定的URL,這個過程通過web服務器直接完成继控。如果我們能把這個URL請求的派遣功能交給框架管理械馆,這對我們來說會非常靈活,即框架的路由功能武通。

把單個php腳本文件暴露給客戶端用戶霹崎,是一種叫做 front controller 設計模式。
這樣的腳本文件類似下面這種:

// framework/front.php
require_once __DIR__.'/vendor/autoload.php';

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

$request = Request::createFromGlobals();
$response = new Response();
$map = array(
    '/hello' => __DIR__.'/hello.php',
    '/bye'   => __DIR__.'/bye.php',
);
$path = $request->getPathInfo();
if (isset($map[$path])) {
    require $map[$path];
} else {
    $response->setStatusCode(404);
    $response->setContent('Not Found');
}
$response->send();

hello.php的例程

// framework/hello.php
$input = $request->get('name', 'World');
$response->setContent(sprintf('Hello %s', htmlspecialchars($input, ENT_QUOTES, 'UTF-8')));

在 front.php 腳本中冶忱,$map 變量把URL和對應的php腳本文件聯(lián)系起來尾菇。

題外話,假如客戶端請求一個路徑,但是這個路徑沒有在 $map 變量中定義派诬,我們則需要返回一個自定義的404頁面劳淆;現(xiàn)在你自己已經可以控制網(wǎng)站了。

如果要訪問某個頁面默赂,你必須在 front.php 腳本中定義沛鸵。

http://127.0.0.1:4321/front.php/hello?name=Fabien
http://127.0.0.1:4321/front.php/bye
/path 和 /bye 是頁面的路徑。

大多數(shù)的 web 服務器比如 Apache 或者 Nginx 都具有重寫請求地址的功能缆八,把 front controller 去掉曲掰,用戶只要輸入 http://127.0.0.1:4321/hello?name=Fabien
就可以直接訪問。

使用 Request::getPathInfo() 能夠獲取去除 front controller 的路徑地址奈辰。

你甚至不需要通過啟動服務器來測試代碼栏妖,采用 $request = Request::create('/hello?name=Fabien'); 即可生成自定義的請求,參數(shù)即自定義的URL路徑奖恰。

現(xiàn)在所有的頁面都會先訪問統(tǒng)一的腳本文件(front.php)吊趾,然后通過把所有其他的代碼放到公共訪問得到目錄以外的地方,可以提高網(wǎng)站的安全性瑟啃。

example.com
├── composer.json
├── composer.lock
├── src
│   └── pages
│       ├── hello.php
│       └── bye.php
├── vendor
│   └── autoload.php
└── web
    └── front.php

配置web服務器的根目錄到 web/论泛,這樣其他的文件將不會被客戶端直接訪問。
我們在瀏覽器測試(http://localhost:4321/?name=Fabien)翰守,運行 php 自建的服務器:

$ php -S 127.0.0.1:4321 -t web/ web/front.php

未完待續(xù)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末孵奶,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子蜡峰,更是在濱河造成了極大的恐慌,老刑警劉巖朗恳,帶你破解...
    沈念sama閱讀 212,686評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件湿颅,死亡現(xiàn)場離奇詭異,居然都是意外死亡粥诫,警方通過查閱死者的電腦和手機油航,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,668評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來怀浆,“玉大人谊囚,你說我怎么就攤上這事≈瓷模” “怎么了镰踏?”我有些...
    開封第一講書人閱讀 158,160評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長沙合。 經常有香客問我奠伪,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,736評論 1 284
  • 正文 為了忘掉前任绊率,我火速辦了婚禮谨敛,結果婚禮上,老公的妹妹穿的比我還像新娘滤否。我一直安慰自己脸狸,他們只是感情好,可當我...
    茶點故事閱讀 65,847評論 6 386
  • 文/花漫 我一把揭開白布藐俺。 她就那樣靜靜地躺著肥惭,像睡著了一般。 火紅的嫁衣襯著肌膚如雪紊搪。 梳的紋絲不亂的頭發(fā)上蜜葱,一...
    開封第一講書人閱讀 50,043評論 1 291
  • 那天,我揣著相機與錄音耀石,去河邊找鬼牵囤。 笑死,一個胖子當著我的面吹牛滞伟,可吹牛的內容都是我干的揭鳞。 我是一名探鬼主播,決...
    沈念sama閱讀 39,129評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼梆奈,長吁一口氣:“原來是場噩夢啊……” “哼野崇!你這毒婦竟也來了?” 一聲冷哼從身側響起亩钟,我...
    開封第一講書人閱讀 37,872評論 0 268
  • 序言:老撾萬榮一對情侶失蹤乓梨,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后清酥,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體扶镀,經...
    沈念sama閱讀 44,318評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡薇正,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,645評論 2 327
  • 正文 我和宋清朗相戀三年硬毕,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片辨液。...
    茶點故事閱讀 38,777評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡辱志,死狀恐怖蝠筑,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情揩懒,我是刑警寧澤什乙,帶...
    沈念sama閱讀 34,470評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站旭从,受9級特大地震影響稳强,放射性物質發(fā)生泄漏场仲。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,126評論 3 317
  • 文/蒙蒙 一退疫、第九天 我趴在偏房一處隱蔽的房頂上張望渠缕。 院中可真熱鬧,春花似錦褒繁、人聲如沸亦鳞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,861評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽燕差。三九已至,卻和暖如春坝冕,著一層夾襖步出監(jiān)牢的瞬間徒探,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,095評論 1 267
  • 我被黑心中介騙來泰國打工喂窟, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留测暗,地道東北人。 一個月前我還...
    沈念sama閱讀 46,589評論 2 362
  • 正文 我出身青樓磨澡,卻偏偏與公主長得像碗啄,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子稳摄,可洞房花燭夜當晚...
    茶點故事閱讀 43,687評論 2 351

推薦閱讀更多精彩內容

  • Composer Repositories Composer源 Firegento - Magento模塊Comp...
    零一間閱讀 3,956評論 1 66
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,858評論 25 707
  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理稚字,服務發(fā)現(xiàn),斷路器厦酬,智...
    卡卡羅2017閱讀 134,637評論 18 139
  • 夜?jié)u愈涼 空氣是清涼的 夜空是寂寥的 路人是匆忙的 穿著臃腫的衣服 埋著頭胆描,呵著氣 路燈閃著斷斷續(xù)續(xù)的光 這般陳舊...
    688a2e4be2af閱讀 139評論 0 3
  • 堅持星球,彼此加油弃锐,大家好袄友,我是姣姣,很高興今天能在這里和大家分享霹菊。大家可以先看下我的身材,是不是很瘦呢支竹?我現(xiàn)在的...
    小饅頭0601閱讀 441評論 0 0