PHP 的 O 语法

v0.4 2024-04-18 20:15 UTC

This package is auto-updated.

Last update: 2024-09-07 18:54:48 UTC


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("&#x213A;", "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攻击。要使用这些功能

  1. 将以下代码放入您的表单中

    <input type="hidden" name="csrftoken" value="" />

  2. 将处理表单的所有内容放入此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()函数