PHP 的 O 语法
Requires
- php: >=8.1.0
- ext-mbstring: *
README
PHP 的 O 语法
这是在元编程 PHP 方面的一次实验,以给它一个更合理的 API。此库需要 PHP 8.1 和 mbstring 模块。
要开始使用它,请将以下代码包含在页面顶部
<?php include "O.php";
也可以单独加载下面描述的每个组件
include("path/to/O/StringClass.php");
echo O\s("foo")->replace("foo", "bar");
目录
字符串和数组
O 的核心是三个字母的函数:s()、a() 和 o()。
s() 函数用于向字符串添加方法
echo s("abc")->len();
// 3
echo s("abc")->substr(2);
// c
s() 将所有标准字符串函数转换为具有相同行为的 方法
s($haystack)->pos($needle)替换为str_pos($haystack, $needle),以及->ipos()、->rpos()和->ripos()s($haystack)->explode($delimiter)替换为explode($delimiter, $haystack)- 类似地:
->trim()、->ltrim()、->rtrim()、->pad()、->len()、->tolower()、->toupper()、->substr()、->replace()、->ireplace()、->preg_match()、->preg_match_all()、->preg_replace()、->in_array()。 - 最后,
->html()是html_special_chars()的安全包装。
支持数组索引运算符
- 在 PHP 5.3 中:
$s = s("abc"); echo $s[2]; // "c" - 在 PHP 5.4 中:
echo s("abc")[2]; // "c"
基本上,这是标准的 PHP 字符串 API,具有以下优势
- 方法语法消除了 haystack/needle 的混淆。
- 一切都是 UTF-8 兼容的,使用 mbstring 函数和自动字符集头。
s() 函数还实现了 JavaScript 的字符串 API
->charAt()、->indexOf()、->lastIndexOf()、->match()、->replace()、->split()、->substr()、->substring()、->toLowerCase()、->toUpperCase()、->trim()、->trimLeft()、->trimRight() 和 ->valueOf()。
a() 对数组执行同样的操作。
实现的方法包括:->count()、->has()(代替 in_array())、->search()、->shift()、->unshift()、->key_exists()、->implode()、->keys()、->values()、->pop()、->push()、->slice()、->splice()、->merge()、->map()、-reduce()、->sum()、->begin()(代替 reset())、->next()、->current()、->each() 和 ->end()。
s() 函数的示例,字符串相似度算法
// adapted from
// http://cambiatablog.wordpress.com/2011/03/25/algorithm-for-string-similarity-better-than-levenshtein-and-similar_text/
function stringCompare ($a, $b) {
$a = s($a); $b = s($b);
$i = 0;
$segmentCount = 0;
$segments = array();
$segment = '';
while ($i < $a->len()) {
if ($b->pos($a[$i]) !== FALSE) {
$segment = $segment.$a[$i];
if ($b->pos($segment) !== FALSE) {
$segmentPosA = $i - s($segment)->len() + 1;
$segmentPosB = $b->pos($segment);
$positionDiff = abs($segmentPosA - $segmentPosB);
$positionFactor = ($a->len() - $positionDiff) / $b->len();
$lengthFactor = s($segment)->len()/$a->len();
$segments[$segmentCount] = array(
'segment' => $segment,
'score' => ($positionFactor * $lengthFactor)
);
} else {
$segment = '';
$i--;
$segmentCount++;
};
} else {
$segment = '';
$segmentCount++;
};
$i++;
};
$getScoreFn = function($v) { return $v['score']; };
$totalScore = a(a($segments)->map($getScoreFn))->sum();
return $totalScore;
}
echo stringCompare("joeri", "jori"); // 0.9
$looksLikeO = mb_convert_encoding("℺", "UTF-8", "HTML-ENTITIES");
echo stringCompare("joeri", "j".$looksLikeO."eri"); // 0.8
注意,最后一行证明 s() 方法是 UTF-8 兼容的,因为 0.8 表示一个字符的差别。
对象和类型
o() 函数用于将数组或字符串转换为对象。字符串被视为 JSON 数据。
$o = O\o('{"key":"value"}');
echo $o->key; // outputs "value"
它可以用来将对象或 JSON 数据转换为定义的类型
class IntKeyValue {
/** @var int */
public $key;
};
$o = o('{"key":"5"}')->cast("IntKeyValue");
echo getclass($o) . " " . gettype($o->key); // O\IntKeyValue integer
你转换到的类上的属性可以具有类型注解,用于描述要转换的类型
/** @var float */:转换为数字/** @var string[] */:转换为字符串数组/** @var MyType */:转换为嵌套复杂类型/** @var array[int]MyType */:转换为具有 int 键和 MyType 值的数组
支持的原始类型包括
- void:变为NULL
- bool/布尔
- int/整数:如果转换失败则变为NULL
- float/double:如果转换失败则变为NULL
- 字符串
- mixed:不进行转换
- resource:如果不是资源则变为NULL
- object:使用o()方法转换为stdObject(接受JSON字符串)
- DateTime:将字符串或整数转换为DateTime实例
任何无法转换为正确类型的数据都将变为NULL。这是一种强制JSON输入为正确类型的简单方法。
技巧
- 您可以使用
convertValue($value, $type)将任何值转换为任何类型。这个->cast()方法是对这个功能的方便包装。 - 您可以通过将其转换为自身类型来“修复”任何对象上属性的类型。(例如,确保整数不是秘密的字符串)
您还可以通过将其转换为定义的类型来过滤$_REQUEST数组
class RequestParams {
/** @var string */
public $foo = "";
/** @var int */
public $bar = 1;
}
$request = o($_REQUEST)->cast("RequestParams");
print_r($request);
当您用?foo=test调用该脚本时,它将输出
O\RequestParams Object
(
[foo] => test
[bar] => 1
)
foo参数是从$_REQUEST数组中获取的,但bar从类型定义中获取其默认值。如果指定了bar,则它将自动尝试转换为int(否则变为NULL)。
输入验证
如前一小节所指出的,o()->cast()方法是一种强制输入为特定类型的方便方法。然而,为了确保输入的有效性,您不仅要验证类型,还要验证值的范围。
为此,php-o实现了JSR-303(Java Bean Validation)。
用例子来说明最容易
class Validatable {
/**
* @var string
* @NotNull
* @Size(min=0,max=5)
*/
public $text = "";
/**
* @var float
* @Min(0)
*/
public $positive = 0;
}
$obj = o(array("text" => "123456", "positive" => -1))->cast("Validatable");
$validation = Validator::validate($obj);
print_r($validation);
这将输出以下错误
Array
(
[0] => O\ConstraintViolation Object
(
[message] => Size must be between 0 and 5
[constraint] => Size
[rootObject] => O\Validatable Object
(
[text] => 123456
[positive] => -1
)
[propertyPath] => text
[invalidValue] => 123456
)
[1] => O\ConstraintViolation Object
(
[message] => Must be >= 0
[constraint] => Min
[rootObject] => O\Validatable Object
(
[text] => 123456
[positive] => -1
)
[propertyPath] => positive
[invalidValue] => -1
)
)
简而言之,Validator::validate方法将执行每个属性注释中指定的检查。结果是包含验证错误的数组,如果所有属性都有效,则此数组为空。
支持的注释有这些
- @Null:可以用于在子类中覆盖@NotNull。
- @NotNull:属性不接受NULL作为值。如果您不指定此属性,NULL是一个有效的值(即使它违反其他验证规则)。
- @NotEmpty:与@NotNull相同,并且字符串不能为""或空白,数组必须至少有一个项。
- @AssertTrue
- @AssertFalse
- @Min(value):属性必须大于等于value
- @Max(value):属性必须小于等于value
- @Size(min=value,max=value):数组或字符串长度必须符合这些约束(支持仅指定min或max)
- @DecimalMin(value):与@Min相同,但可以处理大数字
- @DecimalMax(value):与@Max相同,但可以处理大数字
- @Digits(integer=value,fraction=value):指定的数字最多有这么多整数或小数位数
- @Pattern(regex=value):字符串必须匹配正则表达式
- @Past:日期必须在过去(支持DateTime实例、日期字符串和整数时间戳)
- @Future:日期必须在将来
- @Valid:递归验证此属性(对于具有类型注释的对象属性)
Validator是一个可插拔框架。您可以轻松添加自己的注释。查看O源代码以了解如何操作。
链式调用
c()函数在默认情况下不是流畅API的对象上实现了流畅API。换句话说,它包装了一个对象,使得该对象的方法返回一个可连缀的对象。
这意味着您可以做类似以下的事情
echo c(s("ababa"))->explode("b")->implode("c");
// outputs acaca
简而言之,您可以对方法进行jQuery样式的链式调用。
如果您想获取链式操作中的对象,可以使用->raw()方法
$s = c(s("123abcxxx"))->substr(3)->rtrim("x")->raw();
// $s === "abc"
您可以在任何类型上使用c()函数,而不仅仅是O提供的特殊类型(例如,在DateTime类型上)。如果返回值是原始类型(如字符串或数组),则会将其转换为智能类型
echo c(new \DateTime())->format("Y-m-d")->explode("-")->pop();
// contrived example to output the current day
提供了简写函数
- cs() == c(s())
- ca() == c(a())
- co() == c(o())
会话处理
O默认设置了会话,使其安全
当您进行session_start()时,O确保以下内容:
- 会话cookie具有httpOnly标志,如果会话是通过HTTPS创建的,则还具有安全标志
- 会话id不会在URL中传递,而只通过cookie传递
- 会话名称已从默认名称更改
- 在第一次请求时更改会话id,以防止会话固定
提供了一些便利功能,以防止CSRF攻击。要使用这些功能
-
将以下代码放入您的表单中
<input type="hidden" name="csrftoken" value="" />
-
将处理表单的所有内容放入此if语句中
if (is_csrf_protected()) { ...
虽然这不是完美的,但应足以作为基本预防措施。
还有一个会话包装类,可以使其具有面向对象的味道
$session = new Session();
echo $session->foo; // == $_SESSION["foo"], isset implicitly performed
echo $session->getCSRFToken(); // == get_csrf_token();
if (!$session->isCSRFProtected()) die(); // == is_csrf_protected();
PDO
PDO被包装以提高其API。
将PDO语句的fetch方法直接添加到PDO对象中
$db = new O\PDO("sqlite::memory:");
$rows = $db->fetchAll(
"select * from test where id <> :id",
array("id" => 2) // bound parameter by name
);
$row = $db->fetchRow(
"select * from test where id = ?",
array(3) // bound parameter by position
);
$col = $db->fetchColumn(
"select description, id from test where id <> :id",
array("id" => 1), // NULL to skip
1 // return second column
);
它还添加了一个新的fetch方法,用于获取单个值
$value = $db->fetchOne(
"select description from test where id = :id",
array("id" => 2)
);
参数绑定支持绑定多个参数。
匿名绑定
$value = $db->prepare(
"select count(*) from test where id <> ? and id <> ?"
)->bindParams(array(2, 3))->execute()->fetchColumn(0);
命名绑定(通过对象或关联数组)
$params = new StdClass();
$params->id = 4;
$params->desc = "foo";
$stmt = $db->prepare(
"select description from test where id = :id and description <> :desc");
$value = $stmt->bindParams($params)->execute()->fetchColumn(0);
请注意,API是流畅的(允许链式调用)。
您可以通过fluent选项禁用此功能
$db = new O\PDO("sqlite::memory:", "", "", array("fluent" => false));
还有用于基本CRUD操作的高级方法
$insertId = $db->insert(
"test", // table
array("description" => "foo")
);
// uses PDO::lastInsertId to return the id
$count = $db->update(
"test",
array("description" => "foo"), // set to this
"id >= :id1 and id <= :id2", // where
array("id1" => 2, "id2" => 6) // where parameters
);
$count = $db->delete(
"test",
"id >= :id1 and id <= :id2",
array("id1" => 2, "id2" => 6)
);
您可以附加一个分析器以获取每个查询的配置文件
$profiler = new O\PDOProfiler();
$db->setProfiler($profiler);
$db->query("select count(*) from test where id = :id", array("id" => 6));
$profiles = $profiler->getProfiles();
print_r($profiles);
-->
Array(
[0] => Array(
[0] => 7.7009201049805E-5 = elapsed time
[1] => 1419201522.886 = start time
[2] => select count(*) from test where id = :id
[3] => Array([:id] => 6)
)
)
文件
TODO:记录新的FileClass和f()函数