polyfony-inc/polyfony

Polyfony 是一个简单且强大的 PHP 微框架

dev-master 2024-09-13 12:18 UTC

This package is auto-updated.

Last update: 2024-09-30 01:30:21 UTC


README

SensioLabsInsight Maintainability

Polyfony 是一个直观、轻量且强大的 PHP 微框架。

哲学

受 Symfony 和 Laravel 启发,但更倾向于极端的简洁和效率。
与主要 PHP 框架相比,Polyfony 覆盖了我们大部分时间所需功能的 95% 以上,并且只使用了主要框架所需资源、空间、配置文件和依赖的一小部分。
它具有路由、组件、控制器、分析器、视图、ORM、测试、缓存、多语言、事件、身份验证、环境、表单助手、命令行助手等功能,并通过 composer 提供可扩展性。

"Hello World" 的足迹

  • ≤ 300 Ko 的磁盘空间 (35% 的注释行)
  • ≤ 400 Ko 的 RAM
  • ≤ 2.5 ms (冷启动)

要求

您需要一个 POSIX 兼容的系统(Linux/MacOS/xBSD)、PHP >= 7.4 以及 ext-pdo、ext-sqlite3、ext-mbstring、ext-msgpack 和您 web 服务器的重写模块。

免责声明

如果您考虑使用此框架代替 Laravel 等主要且受支持的框架,那么您的决策过程和/或项目评估中可能存在一些严重问题。

在几乎所有情况下,使用主流框架都是一个更好的选择。

安装

your-project-folder 中下载并预配置框架
composer create-project --stability=dev polyfony-inc/polyfony your-project-folder

--stability=dev 是必需的,因为我们尚未发布版本 大多数通过 composer 安装的依赖项仅由 PHPUnit 需要。

NginX 配置
root /var/www/your-project-folder/Public
location / {
	try_files $uri $uri/ /index.php?$query_string;
}
LigHTTPd 配置
server.document-root 	= "/var/www/your-project-folder/Public/"
url.rewrite-once 		= ("^(?!/Assets/).*" => "/?")
Apache 配置
DocumentRoot "/var/www/your-project-folder/Public/"

几乎不需要学习

readme.md 文件应该足以让您入门。您还可以浏览 Private/Bundles/Demo/ 组件和框架的源代码。由于框架类是静态的,所有内容都通过简单和自然的命名在任何地方始终可用。

下面的代码假设您在每个调用之前都在 Polyfony 命名空间前添加了前缀。

请求

// retrieve an url parameter
Request::get('blog_post_id');

// retrieve a posted field named `search_expression`
Request::post('search_expression');

// retrieve a posted file
Request::files('attachment_document');

// retrieve a request header
Request::header('Accept-Encoding');

// retrieve the user agent
Request::server('HTTP_USER_AGENT');

// check if the method is post (returns a boolean)
Request::isPost();

// check if the request is done using ajax (returns a boolean)
Request::isAjax();

// check if the request is done thru TLS/SSL (returns a boolean)
Request::isSecure();

// check if the request is from the command line (returns a boolean)
Request::isCli();

数据库

Polyfony 提供了自感知实体,类似于 RubyOnRails 中的 ActiveRecordEloquent 对象。

以下示例假设数据库中存在名为 Pages 的表。在 Models\Pages 文件中需要以下最小量的代码。

namespace Models;
class Pages extends \Polyfony\Entity {}
// Retrieve a database entity by its ID, here, the id 67
$webpage = new Pages(67);

// Retrieve another database entity by its `url` column
$webpage = new Pages(['url'=>'/my-awesome-vegan-burger-recipe/']);

// Retrieve a single Entity by its ID and generate an input to change its title property, with a custom css class
// note that any html in the title field will be escaped in the <input> to prevent XSS
// returns <input type="etextmail" name="Pages[title]" value="My awesome Vegan burger recipe is so yummy" />
(new Pages(67))
	->input('title', ['class'=>'form-control']);

// Create an new page, populate and save it
(new Pages)
	->set([
		'url'				=> '/veganaise-c-est-comme-de-la-mayonnaise-mais-vegan/',
		'title'				=> 'I\'m running out of ideas...',
		'description'		=> 'Meta descriptions get less and less important with Google\'s newest algorithms',
		'creation_date'		=> '18/04/1995', // this gets converted to a unixepoch automagically
		'modification_date'	=> time(), 
		'contents'			=> 'Meh...',
		'categories_array'	=> ['Cooking', 'Organic'], // this get's saved as json automagically
		'id_creator'		=> Security::getAccount()?->get('id') // assuming you are logged in
	])
	->save();

// Alternatively, you can also create an entity this way
Pages::create([
	'url'		=>'...',
	'title'		=>'...',
	// more columns and values...
]);

// Retrieve the `title` and `id` of 5 pages that have the `Organic` category, 
// that have been modified in the last week, 
// and that have been created by user's id 7.
$pages = Pages::_select(['title','id'])
	->whereContains(['categories_array'=>'Organic'])
	->whereGreaterThan('modification_date', time() - 7*24*3600)
	->where(['id_creator'=>7])
	->limitTo(0,5)
	->execute();

参数

->where()			// == $value
->whereNot()			// <> $value
->whereBetween()		// BETWEEN $min_value AND $max_value
->whereMatch()			// MATCH column AGAINST $value
->whereContains()		// % $value %
->whereEndsWith()		// % $value
->whereStartsWith() 		// $value %
->whereNotEmpty() 		// <> '' and NOT NULL
->whereEmpty() 			// '' or NULL
->whereNotNull() 		// NOT NULL
->whereNull() 			// NULL
->whereGreaterThan() 		// < $value
->whereLessThan() 		// > $value
->whereIn() 			// IN ( values )
->whereNotIn() 			// NOT IN ( values )

选项

->orderBy()				// associative array ('column'=>'ASC')
->limitTo()				// $start, $end
->groupBy()				// ?
->first()				// return the first Entity instead of an array of Entities

魔法列

  • _date_on_at 结尾的列将从 DD/MM/YYYY 转换为时间戳,反之亦然
  • _datetime 结尾的列将从 DD/MM/YYYY HH:mm 转换为时间戳,反之亦然
  • _array 结尾的列将转换为 json 并存储,然后恢复到其原始类型
  • _size 结尾的列将转换为可读的 human readable 大小

您可以轻松地向 _array 列的末尾添加元素。假设您有一个 Process 对象/表,它有一个 events_array 属性/列。

// create a new Process object
(new Process)
	// push an event into the events_array object
	->push('events_array', [
		// this array is arbitrary, you are free to push anything into the column
		'date'			=>time(),
		'is_important'	=>false,
		'message'		=>'Something just happened !'
	])
	// your can also ommit the _array, the framework will find the right column
	->push('events', [
		// this array is arbitrary, you are free to push anything into the column
		'date'			=>time(),
		'is_important'	=>true,
		'message'		=>'Something dubious just occured !'
	])
	->save();

实体访问器

实体具有基本的 ->set([$column=>$value])->get($column, $bymypass_html_entities_protection=false) 方法。此外,还有

  • ->oset($associative_array, $columns_to_actualy_set) "OnlySet",某些列
  • ->lget($column) "LocalizedGet",将尝试使用区域设置来获取返回值
  • ->tget($column, $length) "TruncatedGet",将截断超出长度的返回值

跨站脚本(XSS)防护

在任意其他列上调用 ->get() 将会自动使用 PHP 的 FILTER_SANITIZE_FULL_SPECIAL_CHARS 对特殊 HTML 符号进行转义,以防止 XSS 攻击。如果实际上你需要从数据库中获取原始数据,请在第二个参数中添加 true,如下所示 $object->get('column_name', true); 来检索数据 "原样"。在代码中的任何地方调用 Format::htmlSafe() 都会提供相同的转义功能。

数据验证器

应该使用 symfony/validatorrespect/validationwixel/gump 或类似的包来管理数据验证。 话虽如此,有一个非常基础的(可选的)内置验证器,用于防止在操作对象时数据损坏。

要强制执行它,在你的模型中声明一个名为 VALIDATORS 的常量数组,其中每个键是列名,每个值是一个正则表达式、一个允许值的数组或一个标准的 PHP 过滤器名称(例如 FILTER_VALIDATE_IP)。

  • 示例
Models\Accounts extends Polyfony\Security\Accounts {
// Normal model classes extend Polyfony\Entity. 
// Accounts extends an intermediate (but transparent) class that adds authentication logic.

	const IS_ENABLED = [
		0	=>'No',
		1	=>'Yes'
	];

	const VALIDATORS = [

		// using PHP's built in validators
		'login'					=>FILTER_VALIDATE_EMAIL, 
		'last_login_origin'		=>FILTER_VALIDATE_IP,
		'last_failure_origin'	=>FILTER_VALIDATE_IP,

		// using arrays
		'is_enabled'=>self::IS_ENABLED,
		'id_level'	=>self::ID_LEVEL

	];
}

验证在调用 ->set() 时发生,并将抛出异常。

请注意,您不需要在验证器中包含 NULLEMPTY 值来允许它们。 NULL/NOT NULL 应在数据库中进行配置,以便框架知道哪些列可以是 null,哪些不可以。

请注意,在表上执行批量 ->update(即:不使用不同的对象/实体)将绕过这些验证器。这将在后续版本中修复。

数据过滤

数据过滤和净化可以用来 附加代替 数据验证器。虽然验证器在遇到无效数据时会抛出异常,但数据过滤器会清理数据,使其符合预期数据的性质。

要强制执行数据过滤,在你的模型中声明一个名为 FILTERS 的常量数组,其中每个键是列名,每个值是一个过滤器名称或一个将要依次应用的过滤器名称的数组。

  • 示例
// an imaginary group model, that represent a group of people
Models\Groups extends Polyfony\Entity {

	const FILTERS = [
		// replaces , with a dot and removes everything except 0-9 + - .
		'group_monthly_allowance'	=> 'numeric', 
		// trim spaces, removes any special chars and capitalize each words
		'group_name'				=> ['trim','text','ucwords'], 
		// removes any special chars and capitalize each words
		'group_manager_name'		=> ['text','strtoupper'], 
		// cleanup an email address
		'group_manager_email'		=> 'email' 
	];

}

过滤在调用 ->set() 时发生,并在验证(如果有)之后。

可用过滤器列表

如果数据的大写字母比例足够低,则 capslock30capslock50capslock70 不会影响数据。此过滤器旨在创建更美观、更干净的数据库,它是为喜欢 FUCK YEAH CAPS LOCK !! 生活方式的老年人设计的。

使用模型过滤器的好处之一是,您的输入和文本区域将自动获得正确的 HTML 属性和类型。

查看以下模型、视图和 HTML 输出。

class User extends Polyfony\Entity {
	const FILTERS = [
		'user_login'=>['email','length128']
	];
}
<?= (new Models\Users->input('user_login')); ?>
<input name="Users[user_login]" type="email" maxlength="128" value="" />

如果列不能为 null,则您的输入还将获得 required="required" 属性。这是从数据库模式中推断出来的。

请注意,在表上执行批量 ->update(即:不使用不同的对象/实体)将绕过这些验证器。这将在后续版本中修复。

数据自动填充

框架将寻找一些常见的列,并尝试使用 Unix 纪元或当前认证用户的 ID 来填充它们。

Entity 创建 时:creation_bycreation_datecreated_bycreated_atcreation_datetime

Entity 修改 时:modification_bymodification_datemodified_bymodified_at

路由器

路由将 URL 映射到一个位于 Bundle 中的 Controller 中的 Action
Action 是一个 方法Controller 是一个 Bundle 是一个 文件夹。路由应在每个包的 Loader 目录中的 Route.php 文件中声明。

示例: Private/Bundles/{BundleName}/Loader/Route.php

路由可以接受多个参数,也可以没有。
  • Router::map('/admin/:what/:id/', 'Bundle/Controller@{what}').
  • Router::map('/url/', 'Bundle/Controller@action').
操作可以
  • 成为URL的参数(如第一个示例。动作将是第二个参数{what}
  • 可以省略。在这种情况下将调用->index()。如果它不存在,将调用->default(),如果它也不存在,则抛出异常。

注意,作为额外的安全措施,动作只能包含字母数字字符、-_.

在调用所需动作之前,将在控制器上调用一个->before()方法。你可以声明一个,或者省略它。
在调用所需动作之后,将在控制器上调用一个->after()方法。你可以声明一个,或者省略它。

  • 以下路由将匹配到/about-us/的GET请求
Router::get('/about-us/', 'Pages/Static@aboutUs');

它将调用Private/Bundles/Pages/Controllers/Static.php->aboutUs();

  • 以下路由将匹配到/admin/{edit,update,delete,create}/和/admin/的任何方法请求(GET、POST...)
Router::map('/admin/:section/:id/', 'Admin/Main@{section}')
	->where([
		'section'=>[
			// section must be one of the following four
			// though you can ommit this, a wrong section value will trigger a defaultAction() on the controller
			// which means an exception if you have not declared it.
			'in_array'=>['edit','update','delete','create']
		],
		'id'=>[
			// id has to be a numeric value
			'is_numeric', 		
			// id can't be 0
			'!in_array'=>[0] 	
		]
	]);

它将调用Private/Bundles/Admin/Controllers/Main.php->{section}();

路由也可以在数据库迭代中动态生成。

// assuming you have a Pages table containing, well, pages. 
// those would have an "url" column that define the absolute url of the page.
// you should replace the _select with a cachable method in the model though.
foreach(Pages::_select(['url'])->execute() as $page) {

	// map that url to a single "Pages" controller 
	// that resides in a "Site" bundle
	Router::get(
		$page->get('url'),
		'Site/Pages@view'
	);

}

然后,在Bundles/Site/Controllers/Pages.php

class PagesController extends Controller {

	public function viewAction() {

		// retrieve your webpage from the database
		// using not its id, but its url column value !
		$page = new Pages(['url'=>Request::getUrl()])

		// maybe set the title and description using that database object
		Response\HTML::set(
			'metas'=>[
				'title'			=>$page->get('title'),
				'description'	=>$page->get('description')
			]
		);

		// and pass your variables to the view
		$this->view('Pages/FromTheDatabase', [
			'title'		=>$page->get('title'),
			'contents'	=>$page->get('contents')
		]);

	}

}
URL参数约束
  • "in_array" => [允许的值]
  • "!in_array" => [不允许的值]
  • "preg_match" => "要匹配的正则表达式"
  • "!preg_match" => "不要匹配的正则表达式"
  • "is_numeric"
  • "!is_numeric" 如果声明了多个约束,它们都必须匹配。
重定向声明
  • 以下将从/some-old-url/重定向到/the-new-url/,使用301状态码。
Router::redirect('/some-old-url/', '/the-new-url/', [$status_code=301]);

这些都是静态重定向,不是重写规则。它们不能包含动态参数。

URL参数签名

有时,你可能想与一个没有你软件账户的人分享链接。你可能不想麻烦这些人注册账户。在这些情况下,签名URL参数并传递哈希是一种保护URL的好方法。生成的URL将是唯一的且不可预测的。

要要求路由签名,将->sign()方法应用于你的路由声明。

// the associated signed URL can be sent by email
// the client would not have to log in to track their order, how comfortable
Router::get(
	'/track/shipping/:id_client/:id_order/'
	'External/Tracking@order',
	'order-tracking'
)->sign();

要获取签名URL,使用Router::reverse()方法,生成的URL将自动签名。

// this will generate an URL looking like this 
// https://my.domain.com/track/shipping/4289/24389/E29E798A097F099827/
Router::reverse(
	'order-tracking'
	[
		'id_client'=>$client->get('id'),
		'id_order'=>$order->get('id')
	], 
	true, 	// force TLS
	true 	// include domain name
);

你可以在Config.ini中的[router]下自定义哈希参数的名称,然后在signing_parameter_name = 'url_parameters_hash',这样它就不会与你的命名参数冲突。

速率限制

默认情况下,节流基于路由名称和远程地址(IP),因此你必须命名你的路由。使用的机制是漏桶

要节流路由

Router::put(
	'/place-order/:id_customer/'
	'External/Tracking@order',
	'place-order'
)->throttle(2, 3600); // enforces a limit of 2 requests every 3600 minutes (one hour, leaky bucket)

要手动节流(在控制器中)

// you can use the method enforce (this limits to 5 per hour)
Throttle::enforce(5, 3600);

// method shortcuts (this limits to 2 per minute)
Throttle::perMinute(2); 
//Throttle::perSecond();
//Throttle::perHour();
//Throttle::perDay();

// and even define your own keys (here, the lookup table will use a hash of id_client+IP instead of the IP address + route name)
Throttle::perHour(
	2, 
	Hashs::get([
		$id_client, 
		Request::server('REMOTE_ADDR')
	])
);

当有人正在节流时,将抛出一个带有消息“您正在被速率限制”的429异常。请注意

  • 在节流期间的新点击不会扩展锁定
  • 没有爆发支持
  • 后端使用APCu,出于性能和DoS安全的原因

环境

https://github.com/polyfony-inc/polyfony/wiki/Reference#class-polyfonyconfig

环境定义了执行上下文,它们有自己的变量集。在Polyfony中存在两个环境

  • Dev,开发环境(这是你的编码发生的地方,很可能是你的本地开发服务器或你的电脑),
  • Prod,生产环境(也称为Live)。

两个环境共有的变量应该放在主配置文件Private/Config/Config.ini中,环境检测可以基于

  • 域名
  • 端口。

你可以在Config.ini中选择检测方法

[polyfony]
detection_method = "domain" ; or "port"

根据检测到的环境,要么是

  • Private/Config/Dev.ini,要么是
  • Private/Config/Prod.ini将覆盖/合并到主Config.ini

与许多框架不同,您的开发应用程序文件夹和生成文件夹完全相同。您不需要在生产服务器上使用不同的.env文件。

以下是带有其开发域的示例Dev.ini
Private/Config/Dev.ini

[router]
domain = project.company.tld.test
port = 80
以及带有其生产域的示例Prod.ini

如果既没有匹配的域也没有端口,框架将回退到生产模式

Private/Config/Prod.ini

[router]
domain = project.company.tld
port 80

[response]
minify = 1
compress = 1
cache = 1
pack_js = 1
pack_css = 1

安装期间,Composer会将默认配置文件和已准备好的设置放入其中

您需要修改您的/etc/hosts文件,将project.company.tld.test指向127.0.0.1或修改您的本地DNS服务器。

要检索配置值(从合并的配置文件中)
// retrieve the whole 'response' group
Config::get('response');

// retrieve only a key from that group
Config::get('response', 'minify');

具有不同的配置文件允许您

  • 设置一个绕过电子邮件来捕获开发环境中发送的所有电子邮件
  • 仅在生产环境中启用压缩、混淆/压缩和缓存
  • 在开发中显示分析器(甚至在早期生产阶段如有必要)
  • 使用不同的数据库配置
  • 在生产中加强安全参数,同时在本地测试期间允许较松的设置
  • 等。

安全

安全性基于一个共同的电子邮件/密码对。密码在存储到数据库之前会被强加密和加盐。可以使用algo(默认为sha512)和salt参数调整哈希算法。

用户可以直接分配任意数量的角色AccountsRoles和权限AccountsPermissions。角色本身也可以分配权限。用户将从其角色继承权限。权限必须至少分组到一个逻辑组AccountsPermissionsGroups中。

此外,两种机制加强安全性

  • 一个节流机制,防止暴力攻击。可以通过forcing_timeframeforcing_maximum_attempts参数进行调整。
  • 一个反cookie窃取机制,检查最初登录的用户是否仍然是同一个。即使他/她拥有正确的会话cookie。可以通过更改enable_signature_verification参数(默认为1 - 启用)来禁用此功能。

您可以根据请求禁用cookie窃取保护。通过动态更改配置Config::set('security','enable_signature_verification', 0),这在特定情况下可能很有用。

要保护一个页面(要求用户登录)
Security::authenticate();

认证失败将抛出一个带有403状态码的异常,并将重定向到Private/Config/Config.ini -> [router] -> login_route = ""

如果页面或操作需要特定的权限
Security::denyUnlessHasPermission('permission_name or permission_id or permission entity');

如果不符合这些要求,将抛出异常,但不会将用户重定向到任何地方。

要手动检查凭据
Security::getAccount()
	->hasPermission(
		$permission_id // or permission_name or permission entity
	);
Security::getAccount()
	->hasRole(
		$role_id // // or role_name or role entity
	);

分析器

在重代码块周围设置标记来估计该块的时间和内存影响。

Profiler::setMarker('ClueA.subclue1');
Profiler::releaseMarker('ClueA.subclue1')

如果Config::get('profiler', 'enable')设置为true (1)且您的Response类型为html,您将在页面底部看到一个漂亮的条形图,其中包含大量有用的信息。该条形图依赖于bootstrap 4 CSS和JS。请确保将它们添加到您的资产中,以享受分析器的所有好处。默认情况下,一些标记放置在关键位置(围绕每个Database查询,围绕控制器转发...)。

如果您的Response类型为json,则Profiler信息将作为一个数组与您的Response合并。

Profiler Demo1

Profiler Demo2

Profiler Demo3

区域设置

地区信息存储在csv文件中(制表符+双引号),存储在每个捆绑包的Bundles/MyBundle/Locales/文件夹中。文件在您第一次请求地区时进行解析。语言将自动检测浏览器语言,您也可以手动设置。

获取当前语言的地区(自动检测)
Locales::get($key)
获取不同语言的地区
Locales::get($key, $language)
设置语言(将在cookie中保存一个月)
Locales::setLanguague($language)

异常

如果有任何异常,将路由到名为“exception”的路径,否则异常将正常抛出。默认状态码为500,您可以指定任何HTTP状态码。禁用缓存的状态码。

Throw new Exception($error_message, $http_status_code);

响应

响应是预先配置的,根据Config.ini来渲染完整的HTML页面(主体、标题、元信息、样式表等)。您可以在运行时更改响应类型和参数。

所有响应中的公共部分,您可以在Response中找到。由于linksscriptsmetas是HTML Response的特定部分,因此这些在子命名空间Response\HTML中设置

重定向
Response::setRedirect($url [, $after_seconds=0])
重定向到上一页
Response::previous()
更改字符集
Response::setCharset('utf-8')
以内联形式输出文件
Response::setType('file')
Response::setContent($file_path)
Response::render()
下载文件
Response::setType('file')
Response::setContent($file_path)
Response::download('Myfilename.ext'[, $force_download=false])
更改状态码(例如,设置为400 Bad Request)

这样做将防止响应被缓存。只有200状态码可以被缓存。

Response::setStatus(400)
输出纯文本
Response::setType('text')
输出HTML

这仅输出您在视图中构建的HTML。

Response::setType('html')

这为您构建一个页面,包括<header><style><script><title>元信息。您构建的HTML将放置在构建页面的<body>中。

Response::setType('html')

注意,更改setType将清除任何缓冲的输出.

输出JSON
Response::setType('json')
Response::setContent(['example'])
Response::render()
添加CSS文件和头部链接
Response\HTML::setLinks(['//maxcdn.bootstrap.ac.cn/bootstrap/3.3.1/css/bootstrap.min.css'])

您还可以指定可选属性,例如media

Response\HTML::setLinks([
	'//maxcdn.bootstrap.ac.cn/bootstrap/3.3.1/css/bootstrap.min.css'=>[
		'media'=>'screen'
	]
])

甚至设置完全不同的链接类型,例如favicon

Response\HTML::setLinks([
	'/Assets/Shared/Svg/Favicon.svg'=>[
		'rel'	=>'icon',
		'sizes'	=>'any',
		'type'	=>'image/svg+xml'
	]
])
添加JS文件(本地或远程)
Response\HTML::setScripts([
	'//code.jqueryjs.cn/jquery-3.3.1.slim.min.js'
	'Shared/myfile.js' // this will import directly from the Bundle/Shared/Assets/Js folder
]) 
添加元标签
Response\HTML::setMetas(['google-site-verification'=>'google-is-watching-you'])
手动使用HTTP/2特性推送和预加载资源
Response::push([
	'/Assets/Css/Shared/common.css'	=>'style',
	'/Assets/Js/Shared/common.js'	=>'script',
	'/Assets/Img/Website/header.wep'=>'image',
]) 
自动推送Response\HTML中声明的所有资源

[response]部分中启用参数push_assets。这将使资源在DOM加载和解析之前被推送,从而加快网页的加载速度。

输出电子表格

您可以选择输出

  • CSV
  • XLS(需要phpoffice/phpspreadsheet)
  • XLSX(需要phpoffice/phpspreadsheet)
Response::setType('xlsx'); // xls or csv
Response::setContent([
	['A','B','C'],
	[1,2,3],
	[4,5,6]
]);
// Response::setContent(Models\Accounts::_select()->execute());
Response::download('Accounts.xlsx');

注意,来自数据库的对象数组将自动转换为数组。

在CSV文件的情况下

您可以通过全局配置传递(可选的)选项。

[response]
csv_delimiter = ','
csv_encloser = '"'

在XLSX文件的情况下

您可以通过全局配置传递(可选的)兼容性选项。

[phpoffice]
office_2003_compatibility = 1
缓存响应的结果(所有输出类型都将被缓存,除了file

请注意,必须在ini配置中启用缓存,发布的Request不被缓存,错误Response也不被缓存。

Response::enableOutputCache($hours);

缓存命中将始终使用少于400 Ko的RAM,并且执行速度非常快,在任何合理的服务器上不到一毫秒。

Response默认提供了一些头部信息此示例的相对较慢是由于文件系统通过wifi是NFS
< HTTP/1.1 200 OK
< X-Powered-By: PHP
< Server: None of your business
< Content-Language: fr
< Content-type: text/html; charset=utf-8
< Content-Length: 11
< X-Memory-Usage: 436.9 Ko
< X-Execution-Time: 13.5 ms
下面的示例显示了与上面相同的Hello World Response,但来自缓存
< HTTP/1.1 200 OK
< X-Powered-By: PHP
< Server: None of your business
< Content-type: text/html; charset=utf-8
< Content-Encoding: gzip
< Content-Length: 31
< X-Footprint: 13.5 ms 436.9 Ko
< X-Environment: Prod
< Date: Mon, 19 Feb 2018 19:54:19 +0100
< Expires: Mon, 19 Feb 2	018 23:54:19 +0100
< X-Cache: hit
< X-Cache-Footprint: 1.2 ms 418.2 Ko

存储

Store接口看起来像这样
Store\Engine::has($variable);
Store\Engine::put($variable, $value [, $overwrite = false]);
Store\Engine::get($variable); 
Store\Engine::remove($variable);
您可以从不同的存储引擎中选择
Store\Cookie
Store\Filesystem
Store\Session
Store\Database
Store\Memcache
Store\Request

最后一个存储引擎仅将您的键值存储在当前请求的时间内。这些引擎中的某些具有比其他更多的功能,但所有都实现了基本接口,可以存储变量、数组或原始数据。

捆绑包配置

存储一些捆绑包特定数据

特定于包的配置应放置在 Bundles/{MyBundle}/Loader/Config.php 中(例如静态列表选项等)。注意,这些配置将与 Config.ini + Dev.ini/Prod.ini 合并,因此所有配置都可通过一个接口访问: Config::{get()/set()}

Config::set($group, $key, $value);
检索值(整个包或子集)
Config::get($group);
Config::get($group, $key);

电子邮件

电子邮件非常简单易用,并基于 PHPMailer 构建。它们扩展了 Entity 对象,因此具有正常数据库条目的几个额外方法,并且可以发送!

(new Models\Emails)
	->set([
		'charset'	=>'utf8', 			// (get its defaults from Config.ini)
		'format'	=>'text', 			// (get its defaults from Config.ini)
		'from_name'	=>'Me me me', 		// (get its defaults from Config.ini)
		'from_email'=>'me@myself.com', 	// (get its defaults from Config.ini)
		'subject'	=>'Hello !',
		'body'		=>'Hello again ?',
		'reply_to'	=>'foo@bar.com',		// fake column
		'to'		=>'email@domain.com', 	// fake column
		'bcc'		=>'email2@domain.com', 	// fake column
		'cc'		=>[ 					// fake column
			'email1'=>'name1',
			'email2'=>'name2'
		],
		'files_array'=>[
			'../Private/Storage/Data/something.pdf'=>'Something.pdf'
		]
	])
	->send([bool $save=false])				// instead of ->send() you can ->save(), to send it later

如果电子邮件发送失败,但使用了 ->send(true),它将出现在电子邮件表中。您可以稍后发送。其 creation_date 列将被填充,但 sending_date 列将为空,这使得稍后重试变得非常简单。

(new Emails(267)) // assuming its id is 267
	->send()

即使没有显式调用 ->save()->send(true),由于电子邮件是从数据库中检索的,发送时,其发送日期列将被更新,并且 它将被保存

使用模板
(new Models\Emails)
	->set([
		'subject'	=>'Another passionnating subject',
		'to'		=>[
			'jack@domain.com'	=>'Jack',
			'jill@domain.com'	=>'Jill'
		],

		// set a PHP view, that is searched for in the "Emails" bundle.
		'view'		=>'OrderConfirmation' // fake column, .php is suffixed automatically
		
		// pass variables to the view, instead of directly setting the body
		'body'		=>[ 
			'firstname'		=>'Louis',
			'order_number'	=>'XH20210722',
			'order_products'=>[$product_1,$product_2]
		],

		// pass any number of CSS files. They will get inlined into style="" attributes of your view
		'css'		=>[
			'OrderConfirmationStyling', // .css is suffixed automatically
		]
	])
	->send(true)
	->isSent() ? do_something() : do_something_else();

您的 Bundles/Emails/Views/OrderConfirmation.php 视图可能看起来像这样

<body>
	
	<h1>
		Order Confirmation N° <?= $order_number; ?>
	</h1>

	<p>
		Hi <?= $firstname; ?>, <br />
		We have received your order and will ship it as soon as possible.
	</p>

	<table class="products-table">
		<?php foreach($products as $product): ?>
			<tr>
				<td>
					<?= $product->getName(); ?>
				</td>
			</tr>
		<?php endforeach; ?>
	</table>
</body>

您的 Bundles/Emails/Assets/Css/OrderConfirmationStyling.css CSS 可能看起来像这样

body {
	background: #efefef;
}
h1 {
	font-size: 16px;
}
p {
	font-weight: bold;
}
table {
	padding: 25px;
}

发送的电子邮件可能看起来像这样

<body style="background: #efefef">
	
	<h1 style="font-size: 16px;">
		Order Confirmation N° XH20210722
	</h1>

	<p style="font-weight: bold;">
		Hi Louis, <br />
		We have received your order and will ship it as soon as possible.
	</p>

	<table style="padding: 25px;">
		<tr>
			<td>
				F1 Steering Wheel signed by Louis Hamilton signed
			</td>
		</tr>
		<tr>
			<td>
				Driving shoes, size 43
			</td>
		</tr>
	</table>
</body>

始终要 严格注意 验证插入到电子邮件(或任何地方)中的用户输入。

元素

创建一个 HTML 标签(类似于 mootools' Element)
<?= Element('img',['src'=>'/img/demo.png'])->set('alt','test'); ?>
<img src="/img/demo.png" alt="test" />
创建一个具有开标签和闭标签的 HTML 元素
$quote = new Element('quote',['text'=>'Assurément, les affaires humaines ne méritent pas le grand sérieux']);
$quote->adopt($image);
<quote>Assurément, les affaires humaines ne méritent pas le grand sérieux<img src="/img/demo.png" alt="test" /></quote>

设置 value 将转义其 HTML,与设置 text 类似。

表单

表单助手允许您构建和预设表单元素,例如。
<?= Form::input($name[, $value=null [, $options=[]]]); ?>
这将构建一个具有 form-control 类的两元素选择框,并预设 Peach。
<?= Form::select('sample', [] 0 => 'Apple', 1 => 'Peach' ], 1, ['class'=>'form-control']); ?>
这将构建一个具有 optgroups 的选择框。

注意,optgroup 被替换为匹配的本地化(如果有),值也被替换为匹配的本地化(如果有)。

<?= Form::select('sample', [
	'food'=>[
		0 => 'Cheese',
		1 => 'Houmus',
		2 => 'Mango'
	],
	'not_food'=>[
		3 => 'Dog',
		4 => 'Cow',
		5 => 'Lizard'
	]
], 3); ?>
<select name="sample">
	<optgroup label="food">
		<option value="0">Cheese</option>
		<option value="1">Houmus</option>
		<option value="2">Mango</option>
	</optgroup>
	<optgroup label="not_food">
		<option value="3" selected="selected">Dog</option>
		<option value="4">Cow</option>
		<option value="5">Lizard</option>
	</optgroup>
</select>

从扩展了 Entity 类的对象(例如:您的模型)中可用快捷键。

根据其 ID 获取一个账户
<?= (new Accounts(1))
	->set('login', 'mylogin@example.com')
	->input('login', ['data-validators'=>'required']); 
?>
<input type="text" name="Accounts[login]" value="mylogin@example.com" data-validators="required"/>

可用元素列表

  • 输入
  • 选择
  • 文本区域
  • 复选框

表单元素的一般语法是: $name, $value, $options 当您从 Entity 获取表单元素时,$name$value 将自动设置,只有 $options 是可用的。选择元素略有不同:$name, $list, $value, $options

要获取密码字段,例如,只需将此添加到您的属性数组中:'type'=>'password'

CRSF 保护

提供 CRSF 保护和多提交保护。

在您的 HTML 表单中间(在视图中)
<form action="" method="post">
<!-- more form here -->

<?= new Polyfony\Form\Token(); ?>

<!-- more form here -->
</form>
在您的控制器中
Polyfony\Form\Token::enforce();

就是这样。

实例化一个 "Token" 对象生成一个唯一的令牌,将其存储在 PHP 会话中,并构建一个 HTML 输入元素。
静态 enforce 方法检查是否已通过 POST 提交请求,如果是,则检查是否存在令牌,并且它与会话中存储的令牌匹配。否则,抛出异常并将用户重定向到上一页。

Captcha 保护

提供 Captcha 提供商,它是对 gregwar/captcha 的封装。

在您的 HTML 表单中间(在视图中)

显示 captcha 图像本身

<?= new Polyfony\Form\Captcha(
	5, // number of characters in the captcha (optional)
	150, // width of the captcha, in px (optional)
	40 // height of the captcha, in px (optional)
); ?>

显示一个用于输入 captcha 的输入框

<?= Polyfony\Form\Captcha::input([
	// html attributes (optional)
	'class'=>'form-control',
	'placeholder'=>'Type the captcha here'
]); ?>
在您的控制器中
Polyfony\Form\Captcha::enforce();

就是这样。

实例化一个 "Captcha" 对象生成一个短语,将其存储在 PHP 会话中,并使用 gregwar/captcha builder 构建 captcha 图像。
静态 enforce 方法检查是否已通过 POST 提交请求,如果是,则检查 captcha 值是否存在,并且它与会话中存储的值匹配。否则,抛出异常并将用户重定向到上一页。您可以手动尝试/捕获异常以避免丢失用户输入的内容,在这种情况下,使用 Captcha::enforce(true) 以防止自动重定向。

数据库结构

该框架已经通过SQLite进行了广泛测试,它可能与其他引擎一起工作,但肯定可以在没有的情况下工作。没有它,您将丢失SecurityEmails存储功能、Store\Database引擎和Logger的数据库功能。

数据库结构可以通过转储SQLite数据库Private/Storage/Database/Polyfony.db来获取。可以在Private/Config/Config.ini中将PDO驱动程序更改为MySQLPostgresODBCPostgres和ODBC不支持Query对象。

日志记录

该框架公开了一个具有以下静态方法的Logger

  • debug(string $message, ?mixed $context) :void (级别 0)
  • info(string $message, ?mixed $context) :void (级别 1)
  • notice(string $message, ?mixed $context) :void (级别 2)
  • warning(string $message, ?mixed $context) :void (级别 3)
  • critical(string $message, ?mixed $context) :void (级别 4)

日志可以发送到文件,或发送到您的数据库(请参阅Config.ini [logger][type])。日志的最低级别可配置(请参阅Config.ini [logger][level]),默认情况下,Dev环境配置为从0级别记录,Prod环境配置为从1级别记录。临界级别日志(4)也可以自动通过电子邮件发送(请参阅Config.ini [logger][mail])。

记录的消息/对象/数组也会自动在分析器中可用

示例

Logger::notice('Something not too worrying just happened');
Logger::debug('Someone did something', $some_kind_of_object);
Logger::critical('Failed to contact remote API', $api_handler);

更新框架

要从项目目录中更新框架,运行以下命令(注意向后不兼容的更改)

第一个和最后一个命令允许您在更新后保留和恢复您的composer.json

git stash
git pull
git stash apply

要从项目目录中更新依赖项,运行此命令

composer update

已弃用、已停止和重命名的功能

版本历史

性能

Polyfony被设计为快速,没有妥协(> 2000 req/s)。如果将一个“便利”工具/功能实现到框架中会导致全局执行时间增加,则要么以更有效的方式实现,要么根本不实现。

安全

代码库很小,简单明了,注释丰富。它使用SensioInsight、CodeClimate、RIPS和Sonar进行审计。

编码标准

Polyfony遵循PSR-0、PSR-1、PSR-4编码标准。它不遵守PSR-2,因为它使用制表符进行缩进。