Symfony:PHP开发框架快速入门教程Php

印迹发布于:2020-1-11 874

1.symfony快速入门

symfony应用代码结构如下:

app/:整个应用的配置,模版,translations。
src/:项目php文件。
vendor/:第三方的依赖文件。
web/:站点的入口路径,必须在域名后面加上这个目录才能访问整个站点,不存在入口文件这一说法。包含资源文件如css,javascript等静态文件,还有些前端的控制器什么的。

在app/目录下的AppKernel.php文件,是整个应用的入口。在这个类里面实现了两个方法,registerBundles()注册束,返回所有注册的bundles,registerContainerConfiguration()注册容器配置,返回配置文件。

什么是bundles束呢?官方解释是束是symfony里面最重要的概念,类似与软件里面的插件。在symfony里面所有的事情都是一个束,从框架到模块,所有写的东西都以束的方式呈现。用symfony的概念来讲一个束是一些php,javascript,css,image文件,一个束来实现一个单一的功能,例如论坛,可以被其他开发者来使用。

在symfony中束是主体要的概念,可以灵活地选择使用那些束,在symfony中AppBundle包含应用需要的所有束。
注册束,registerBundles方法是用来注册束的,每个束是一个包含描述这个束的类。在AppBundle中已经包含了很多和框架相关的束,比如FrameworkBundle,DoctrineBundle,SwiftmailerBundle,AsseticBundle,每个束都可以单独配置。

使用记号@BUNDLE_NAME/path/to/file从束里面引用一个文件,例如@BUNDLE_NAME/path/to/file这个将会引用@AppBundle/Controller/DefaultController.php,源代码中没有看到这个目录。

使用记号BUNDLE_NAME:CONTROLLER_NAME:ACTION_NAME来引用控制器类中的动作,如

AppBundle:Default:index,对应DefaultController的indexAction方法。

在Vendors中包含一些第三方的类库,这里的东西最好不要修改,这些类库是通过composer来管理的,这里包含SwiftMailer类库,DoctrineORM,Twig模版等等。

symfony中有自己的缓存系统,整个系统只有在第一次请求的时候会从配置文件中读取,然后编译成一个php文件放在app/cache里面。在开发环境,symfony在每次修改后自动更新缓存。但是在生产环境,每次修改后需手动删除缓存文件。在开发的时候可能会有各种各样的错误,这个时候就需要在app/log目录中查看日志文件,对修改bug很有用。

命令行接口,在app/console目录下包含一些命令行系统帮助我们修改应用。

2.symfony视图简介

拥抱twing,wting文件是一种能够包含任何文本内容的文件,html,css,javascript,xml,csv。把所有类型的都包含进来,来看看twing中的分隔符。

{{ ... }}打印变量或者表达式的内容

{% ... %}语句块,在模版中控制逻辑,比如for,if,等等,来替代常用<?php?>

{# ... #}注释,奇葩。

在控制器方法中使用render方法来渲染模版,用一个数组来传递变量,如下:

$this->render('default/index.html.twig', array(
'variable_name' => 'variable_value',
));

传递给模版的变量可以是数组,字符串,对象,wting模版使用.来获取对象中的属性,举例如下:

{# 1. 简单的变量 #}
{# $this->render('template.html.twig', array('name' => 'Fabien') ) #}
{{ name }}
{# 2. 数组#}
{# $this->render('template.html.twig', array('user' => array('name' => 'Fabien')) ) #}
{{ user.name }}
{# 还是数组 #}
{{ user['name'] }}
{# 3. 对象#}
{# $this->render('template.html.twig', array('user' => new User('Fabien')) ) #}
{{ user.name }}
{{ user.getName }}
{# 还是对象 #}
{{ user.name() }}
{{ user.getName() }}

装饰模版

通常在模版中分享通用的元素比如头,尾,twing用继承的方式来处理,这个特性可以用来定义模板中的公共部分和块,这样子模版就可以使用了。

在下面的例子中index.html.twig模块继承base.html.twing模板。

{# app/Resources/views/default/index.html.twig #}
{% extends 'base.html.twig' %}
{% block body %}
<h1>Welcome to Symfony!</h1>
{% endblock %}

打开app/Resources/views/base.html.twig这个文件看

{# app/Resources/views/base.html.twig #}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>{% block title %}Welcome!{% endblock %}</title>
{% block stylesheets %}{% endblock %}
<link rel="icon" type="image/x-icon" href="{{ asset('favicon.ico') }}" />
</head>
<body>
{% block body %}{% endblock %}
{% block javascripts %}{% endblock %}
</body>
</html>

block这个标签告诉模板它的子模板将会继承这个部分,注意在父模板中叫body那在子模板中也叫body,在上面的例子中子模板替代了父模板中的body,而不是title和javascript等其他的什么。

标签,过滤,方法

twig的最好的特点是标签,过滤,方法,下面的例子使用这些功能来处理信息,然后输出。

<h1>{{ article.title|capitalize }}</h1>
<p>{{ article.content|striptags|slice(0, 255) }} ...</p>
<p>Tags: {{ article.tags|sort|join(", ") }}</p>
<p>Activate your account before {{ 'next Monday'|date('M j, Y') }}</p>

在twig文档中有详细介绍。

包含其他的模板

在模板中和其他模板分享小片段最好的方式是创建一个新的模板组件,然后在其他的模板中包含它。
想想一下我们要在一个页面中显示一些广告,先来创建一个banner.html.twig组件,如下

{# app/Resources/views/ads/banner.html.twig #}
<div id="ad-banner">
...
</div>

在其他的页面中显示这个广告组件,使用includ方法,如下

{# app/Resources/views/default/index.html.twig #}
{% extends 'base.html.twig' %} {#这里继承父模板#}
{% block body %}
<h1>Welcome to Symfony!</h1>
{{ include('ads/banner.html.twig') }} {#这里include一个小广告组件#}
{% endblock %}

植入其他控制器的模板。

如何在当前模板中植入其他的控制器中的内容,想想一下,在使用ajax,在当前模板中使用其他模板中的一些变量的时候是很有用的,我也是这么想的。

假设创建了一个动作topArticlesAction来显示热门文章,如果我们想在渲染index模板中中的一些html内容来显示这些热门文章,使用下面的方法。

{# app/Resources/views/index.html.twig #}
{{ render(controller('AppBundle:Default:topArticles')) }}

那这个是不是粒度越小越好呢

这里的render()方法和controller()方法使用语法AppBundle:Default:topAritcles来引用Default控制器中的topArticle方法

class DefaultController extends Controller
{
public function topArticlesAction()
{
// look for the most popular articles in the database
$articles = ...;
return $this->render('default/top_articles.html.twig', array(
'articles' => $articles,
));
}
// ...
}

创建链接

链接是网站必不可少的元素,除了使用硬编码url之外path方法可以根据路由配置生成url,这样修改路由配置就可以修改这些链接了。

<a href="{{ path('homepage') }}">Return to homepage</a>

注意这里的homepage是一个配置好的路由哦,不要搞错了。还有一个方法url和path很像,但是它是用来产生绝对的url的方法,这个在链到外部网站和RSS文件,的时候很有用

引用资源文件:images,javascript,stylesheets

不能想想网站里面没有图片,javascript,stylesheets这些会是什么样子。symfony中使用asset资源这个单词来引用资源文件

<link href="{{ asset('css/blog.css') }}" rel="stylesheet" type="text/css" />
<img src="{{ asset('images/logo.png') }}" />

asset方法在/web路径下的文件,详细内容参考其他,又见指针!

还有双花括号和上面的引用变量是一样的。

使用asset方法可以使得引用资源更加方便,原因是这样可以移动应用下的文件夹到任何路径,不需要修改模板中的代码,前提是不移动web里面的内容,不要移动任何文件,资源文件目录是不能移动的,当然不用改asset!

最后

因为有了继承父模板extends,块block,包含其他小模板include('ads/banner.html.twig'),植入控制器render(controller('AppBundle:Default:topArticles'))所以模板很容易按照逻辑来扩展。

3.控制器简介

控制器可以做什么呢,symfony定义成一种请求-响应框架,当发送一个请求symfony创建一个Request对象来包装请求信息,响应是控制器中的动作,响应的结果是html内容。我们可以使用$this->render()方法来返回一个响应结果,也可以返回一个原始的响应结果,例如return new Response('Welcome to Symfony!');,就是一个echo,var_dump么

路由参数

大多数情况下URLs里面都会包含很多请求变量,比如在blog里面去展示一篇文章,一定会传一个文章ID之类的参数,系统才知道到底要显示那一篇文章。在symfony中蚕食被包含在url中比如/blog/read/{article_title}/,这里的参数名字不能重复,这不废话。下面在src/AppBundle/Controller/DefaultController.php文件中信添加一个方法helloAction

/**
* @Route("/hello/{name}", name="hello")
*/
public function helloAction($name)
{
return $this->render('default/hello.html.twig', array(
'name' => $name
));
}

然后在浏览器的url中输入http://localhost:8000/hello/fabien,由于没有default/html.twig这个模板,返回错误。现在创建这个模板app/Resources/views/default/hello.html.twig,如下:

{# app/Resources/views/default/hello.html.twig #}
{% extends 'base.html.twig' %}
{% block body %}
<h1>Hi {{ name }}! Welcome to Symfony!</h1>
{% endblock %}

显示Hi fabien! Welcome to Symfony!,如果输入http://localhost:8000/hello/thomas将显示Hi thomas! Welcome to Symfony!如果输入
http://localhost:8000/hello这样的话将会报错,应为需要一个那么参数

使用格式化输出
现在web中要求输出的不仅仅是html内容,还有RSS文件的XML,webservice,json等等,symfony可以使用_format变量选择多种输出,下面的例子中使用html形式输出,并且给home一个默认值

/**
* @Route("/hello/{name}.{_format}", defaults={"_format"="html"}, name="hello")
*/
public function helloAction($name, $_format)
{
return $this->render('default/hello.'.$_format.'.twig', array(
'name' => $name
));
}

很明显这里如果有另外一种方式输出,就要使用另外一个模板,在上面的例子中如果设置defaults={"_format"="xml"},可能就要新建一个xml模板了hello.xml.twig,如下

<!-- app/Resources/views/default/hello.xml.twig -->
<hello>
<name>{{ name }}</name>
</hello>

twig当模板既可以作为html文件,又可以做xml文件等等。访问http://localhost:8000/hello/fabien,将会看到html页面,因为_format的默认值是html,如果访问http://localhost:8000/hello/fabien.xml,这样将在浏览器中输出一个xml文件的内容。

symfony将会自动选择最合适的内容样式,在@Route标记中使用requirements来表明支持的样式,如下:

/**
* @Route("/hello/{name}.{_format}",
* defaults = {"_format"="html"},
* requirements = { "_format" = "html|xml|json" },
* name = "hello"
* )
*/
public function helloAction($name, $_format)
{
return $this->render('default/hello.'.$_format.'.twig', array(
'name' => $name
));
}

这个hello动作支持三种形式的url如:/hello/fabien.xml /hello/fabien.json /hello/fabien.html,同时也要建三个模板

重定向

如果想重定向到另外一个动作中可以使用redirectToRoute()方法如下:

/**
* @Route("/", name="homepage")
*/
public function indexAction()
{
return $this->redirectToRoute('hello', array('name' => 'Fabien'));
}

redirectToRoute()方法可以跳转到hello动作并传递参数Fabien,我在想如果是其他的控制器呢,你妈,只说一半啊

显示错误信息

错误不可避免,如果是404错误,symfony提供一个错误页,如下:

/**
* @Route("/", name="homepage")
*/
public function indexAction()
{
// ...
throw $this->createNotFoundException();
}

505错误直接抛出一个异常,symfony会直接跳到505错误,

/**
* @Route("/", name="homepage")
*/
public function indexAction()
{
// ...
throw new \Exception('Something went horribly wrong!');
}

那么多的异常,都去定义啊

从请求中获取信息

有时候控制器需要获取用户请求的信息如语言,IP地址,url参数等,这时可以在动作中添加一个Request类型的参数,名字可以随意,同时别忘了添加引用use Symfony\Component\HttpFoundation\Request;,这个是symfony自己定义的一个类型

/**
* @Route("/", name="homepage")
*/
public function indexAction(Request $request)
{
// is it an Ajax request?是否是ajax请求
$isAjax = $request->isXmlHttpRequest();
// what's the preferred language of the user?用户是什么语言环境
$language = $request->getPreferredLanguage(array('en', 'fr'));
// get the value of a $_GET parameter获取$_GET请求
$pageName = $request->query->get('page');
// get the value of a $_POST parameter获取$_POST请求
$pageName = $request->request->get('page');
}

在模板中也可以使用app.request变量来获取这些信息,symfony提供了这些信息,如下:

{{ app.request.query.get('page') }}
{{ app.request.request.get('page') }}

使用session保存数据

即使HTTP协议没有国籍的,里面已经包含了session的定义,symfony还是提供了一个session对象,在两个请求之间symfony通过cookie保存session,在控制器中可以很方便的取到session的值

public function indexAction(Request $request)
{
$session = $request->getSession();
// store an attribute for reuse during a later user request
$session->set('foo', 'bar');
// get the value of a session attribute
$foo = $session->get('foo');
// use a default value if the attribute doesn't exist
$foo = $session->get('foo', 'default_value');
}

还可以存储一个闪存,在下一个请求之前删除这个闪存,这样在跳到下一个动作之前输出这个信息,如下:

public function indexAction(Request $request)
{
// ...
// store a message for the very next request
$this->addFlash('notice', 'Congratulations, your action succeeded!');
}

同理在模板中也可以使用这个闪存

<div>
{{ app.session.flashbag.get('notice') }}
</div>

 

4.Symfony和HTTP基础工具

symfony本意是回归本真:开发一个更快,创建更具健壮性的应用。同时,symfony努力从成熟的技术中汲取最好的部分。通过symfony不仅仅学到框架,还会学到很多的web基础知识。

这一部分解释symfony的哲学,也是web开发的基础:HTTP。不管有任何变成背景和语言基础,这一张是必看章节。

HTTP很简单

http是两台电脑之间进行通讯的一种文本语言,例如在xkcd网站中查看最近的喜剧电影,下面的事情将会发生。浏览器发出请求->xkcd

服务器接到请求->服务器准备html页面->服务器将html页面发到我的浏览器。当然实际用到的语言可能要比这个正规,但仍然是很简单的。HTTP是用来描述的这种文本信息的语言。不论你怎么样来构建应用,目标都是来理解这个请求,然后返回文本形式的响应。

symfony从现实生活中构建,不管理是否意识到这都在你身边发生,通过symfony来学习这些。

第一步:客户端发出请求

web上每一个会话都是从一个请求开始,这个请求是通过一个浏览器,手机app等通过文本的方式,http格式发送出来,浏览器发送出请求,然后就在那等待响应。

一个http-speak实际上长这样:

GET / HTTP/1.1
Host: xkcd.com
Accept: text/html
User-Agent: Mozilla/5.0 (Macintosh)

这个简单的消息里包含了一个请求的所有必须的信息,

第一行是最重要的,它包含两个信息URI和HTTP请求方法,URI表明请求的地址和我们想要的资源,http方法定义了你想如何操作资源

GET 从服务器获取资源
POST 在服务器中创建资源
PUT 向服务器上传资源
DELETE 删除服务器上的资源

可以想想如何删除blog上的一篇文章:DELETE /blog/15 HTTP/1.1

实际上HTTP协议中定义了9中HTTP请求方式,只不过很多没有广泛使用起来,实际上很多的浏览器甚至不支持PUT和DELETE方法。除了第一行,其他行也提供了很多信息,他们合在一起称作HTTP头,其他的信息还包含Host,accept表示接受响应的格式,agent表示客户用来请求的代理信息

第二步:服务端返回响应

一旦服务器接收到请求,它知道要请求什么资源(URI),如何操作资源(method)等等。响应的格式如下:

HTTP/1.1 200 OK
Date: Sat, 02 Apr 2011 21:05:05 GMT
Server: lighttpd/1.4.19
Content-Type: text/html
<html>
<!-- ... HTML for the xkcd comic -->
</html>

第一行包含HTTP响应状态这里是200,状态码传达了请求返回的信息,请求成功与否,是否有错,有不同的状态码,可以查看响应的http状态码。

和HTTP请求差不多的,响应也有一些附带信息,例如一个HTTP响应头里有Content-Type,返回的内容可能以不同的状态例如HTML,XML,JSON,内容类型text/html告诉浏览器要返回的是一段html文本。还有很多类型,有些非常强大,有些响应头可以创建强大的缓存。

请求,响应和web开发

这个请求响应会话是整个web的基础,但是它并不是很复杂。不管我们使用何种语言,建立何种应用web,mobile,json api或者开发的逻辑,最终的目标总是理解这种请求,然后返回合适的响应。symfony努力接近这种事实!

php里的请求响应

在PHP的世界里如何看待请求响应呢,事实上PHP将这个过程抽象化:

$uri = $_SERVER['REQUEST_URI'];
$foo = $_GET['foo'];
header('Content-Type: text/html');
echo 'The URI requested is: '.$uri;
echo 'The value of the "foo" parameter is: '.$foo;

这段程序从从http请求中获取信息,然后创建一个响应。PHP使用全局变量$_SERVER和$_GET来检索请求信息。同样使用header方法来创建一个响应最后输出响应返回给浏览器。

HTTP/1.1 200 OK
Date: Sat, 03 Apr 2011 02:14:33 GMT
Server: Apache/2.2.17 (Unix)
Content-Type: text/html
The URI requested is: /testing?foo=symfony
The value of the "foo" parameter is: symfony

symfony里面的请求响应

symfony通过两个类提供处理请求响应的方法,Request类是处理请求的一个类,通过下面例子来看这个类有什么功能:

use Symfony\Component\HttpFoundation\Request;
$request = Request::createFromGlobals();
// the URI being requested (e.g. /about) minus any query parameters,请求路径,不带参数
$request->getPathInfo();
// retrieve GET and POST variables respectively 获取get,post数据
$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 获取cookie
$request->cookies->get('PHPSESSID');
// retrieve an HTTP request header, with normalized, lowercase keys 使用key获取请求的头,
$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类中有很多功能,细节不用开发者考虑,例如isSecure()方法检查PHP中三个变量判断当前请求是不是安全请求HTTPS

参数包和请求属性

symfony使用query和request来获取php中$_GET和$_POSR的相关信息,这两个对象是参数包对象,很好理解,就是获取各种参数的,都有一些方法get(),has(),all()等。

Request类里面还有个公共的属性attributes,在这个属性中可以获取路由相关的信息,例如_controller,id,甚至是_route。

symfony还有个Response类,这个类中设置响应信息,例子如下:

use Symfony\Component\HttpFoundation\Response;
$response = new Response();
$response->setContent('<html><body><h1>Hello world!</h1></body></html>');
$response->setStatusCode(Response::HTTP_OK);
$response->headers->set('Content-Type', 'text/html');
// prints the HTTP headers followed by the content
$response->send();

在symfony中request和response被称作HttpFundation,这个类完全可以从symfony中分离出来用在其他的框架中,还有处理sessiong的,处理文件的等等其他的类。

从Request到Response的旅程

像HTTP一样,request和response很简单,面临重要的任务是在请求和响应之间的处理,换一种说法是真正工作是编写代码来解释请求,创建响应。

应用可能会发送邮件,处理权限,把数据保存到数据库,渲染html等等,如何处理这些复杂的逻辑同事保持代码可维护性呢?

前端控制器

传统php应用中每一个页面有一个单独的php文件。

index.php
contact.php
blog.php

这样写有一些问题,处理url的时候不够灵活,如果把blog.php修改城news.php那就要修改超链接的属性了,如果很多地方都有这个超链接那就要修改很多的地方了。

/index.php executes index.php
/index.php/contact executes index.php
/index.php/blog executes index.php

symfony中这些前端路由可以处理很多不同请求。前端控制器把不同请求分配到不同地方,几乎所有的web应用都有这个功能例如wordpress。

保持条理清晰

使用前端控制器需要算出那些需要处理的代码,那些需要返回,这需要分析url然后计算出访问那些代码然后处理,这个过程可能会比较麻烦

// index.php
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
$request = Request::createFromGlobals();
$path = $request->getPathInfo(); // the URI path being requested
if (in_array($path, array('', '/'))) {
$response = new Response('Welcome to the homepage.');
} elseif ('/contact' === $path) {
$response = new Response('Contact us');
} else {
$response = new Response('Page not found.', Response::HTTP_NOT_FOUND);
}
$response->send();

处理这种请求会很麻烦,所幸的是symfony就是为解决这个问题而设计的。

Symfony应用的运行流程

 symfony按照相同的方式处理所有的请求,如下

路由系统解释请求然后传递给控制器中的方法最终返回响应对象

在路由配置文件每一个页面都被匹配到不同的php方法上面,这个php方法称作控制,使用请求中的消息,使用symfony中的工具创建一个响应对象。换句话说控制是如何介绍请求和创建响应。


总结:

每一个请求执行一个前端路由文件,路由系统根据请求路径和路由配置文件决定那一个php方法会被执行,如果正确的方法被执行,返回合适的响应。

symfony请求处理示例

这里举一个简单的例子来看看处理过程,假设有一个/contact页面,首先在symfony中的路由配置文件中添加一个入口;
YMAL版本的:

# app/config/routing.yml
contact:
path: /contact
defaults: { _controller: AppBundle:Main:contact }

XML版本的:

<!-- app/config/routing.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing
http://symfony.com/schema/routing/routing-1.0.xsd">
<route id="contact" path="/contact">
<default key="_controller">AppBundle:Main:contact</default>
</route>
</routes>

PHP版本的:

// app/config/routing.php
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
$collection = new RouteCollection();
$collection->add('contact', new Route('/contact', array(
'_controller' => 'AppBundle:Main:contact',
)));
return $collection;


当请求/contact页面的时候,匹配成功,执行控制,然后AppBundle:Main:contact这个语法被解释成指向MainController控制器中的contactAction方法。如下:

// src/AppBundle/Controller/MainController.php
namespace AppBundle\Controller;
use Symfony\Component\HttpFoundation\Response;
class MainController
{
public function contactAction()
{
return new Response('<h1>Contact us!</h1>');
}
}

这是个简单例子,它只返回一段html,并没有渲染某个html文件。

Symfony:专注项目本身,而不是工具

前面讲过所有的web应用都是围绕解释请求返回响应这一目标的,当项目变大了,组织维护难度增加。我们会发现相同一个功能是不是地蹦出来,例如连接数据库,渲染模板,处理用户输入,发送email,验证数据等等。

symfony提供很多的工具来处理这些繁琐的事情,在symfony里没有任何东西可以阻挡你完成这些任务,你只需要了解其中的部分或者所有工具,这就是symfonyframework。

独立的工具箱: Symfony 组件

symfony到底是什么呢?首先symfony包含了20多个类库,可供PHP项目中使用,这些叫symfony组件,举几个例子:
HttpFoundation

包含request,response类和一些处理sessiong,文件上传的类

Routing

处理路由请求的类,可以将请求匹配到某个动作上

Form

一个创建,处理form请求的类

Validator

创建规则验证用户输入是否合乎要求

Templating

渲染模板的工具集,处理模板继承及其他模板任务

Security

处理各种安全请求

Translation

用来翻译字符串的类

这些组件可以单独抽出来放在其他的php项目中,也可以在symfony中添加其他的组件以满足自己的开发需求。
全方位解决方案: Symfony 框架

好了,现在再次问自己一个问题,symfony是什么,symfony是一个php类库来解决两个任务:

1.提供可选择的组件或者第三方组件例如发送邮件等。

2.提供显式的配置和一个胶水类来把一些php片段组合起来

symfony整合各种工具为开发者提供一致的使用体验,symfony框架本身就是一个束,可以被配置和替换。symfony提供强大的工具集,初学者可以快速上手,高级开发者可以超常发挥。





http://www.virplus.com/thread-1168.htm
转载请注明:2020-1-11 于 VirPlus 发表

推荐阅读
最新回复 (0)

    ( 登录 ) 后,可以发表评论!

    返回