一个用于Web开发的PHP框架,具有高性能和灵活性。非常适合团队开发和代码维护。

dev-master 2022-07-19 09:49 UTC

This package is auto-updated.

Last update: 2022-07-19 09:49:13 UTC


README

0.4版本中有什么新功能?

结构变更

将模块结构从 Manager->Package->Controller 改为 App->Domain->Distributor->Module->Controller。新的结构提供了站点到站点的内部API访问,并且对Distributor有清晰的图片和角色。

打包为phar

Razy框架已打包成一个单独的phar文件,这使得源代码更容易维护并提供自更新功能。例如,使用php Razy.phar build在任何位置构建Razy环境,或者使用以下命令添加新站点:

php Razy.phar set yourdomain.com/path/to/ dest_code

Web资产

在v0.3中,模块的Web资产(如cssjsimage)位于它们自己的view目录中,因此Web资产的URL将包含模块路径。换句话说,Web资产文件URL的长度取决于模块目录的深度。实际上,一些开发者可能不喜欢从URL中公开模块或发行商结构。为了满足开发者的安全要求,在v0.4中,有一个名为assets的参数在package.php中,用于将指定的Web资产解包到以发行商代码命名的文件夹中,并更新.htaccess重写规则以隐藏实际的资产位置。

在package.php中,assets参数应如下所示

<?php
return [
	'module_code' => 'yourname.module',
	'api' => 'yourapi',
	'version' => '1.0.0',
	'author' => 'Your Name',
	'assets' => [
		'the/file/under/module/folder' => 'target/folder',
		'specified/file/name.txt' => 'newname.txt',
	],
];

上述列表中的资产将通过setremovelinkunlinkunpackassetfix命令从Razy.phar通过CLI克隆到view\{$dist_code}目录下。

模块类命名的新规则

为了防止通过内部API在不同的发行商之间访问模块时的重声明错误,Razy为每个Module引入了新的命名规则。所有模块都必须以Razy\Module\{Distributor_Code}\{Module_Class}开始的命名空间。

假设发行商代码Main模块代码root

在v0.4之前

namespace \Razy\Module\root;

在v0.4之后

namespace \Razy\Module\Main\root;
/**
 * Now you can also use fqdn format as the module code like `this.is.root`,
 * and the namespace of the module class should be:
 */
namespace \Razy\Module\Main\this\is\root;

请注意,发行商代码格式应该是纯字母和数字。

Razy v0.4有一个新功能,您可以加载不同发行商中的共享模块,为了避免重声明类错误,您应该使用以下命名空间

namespace \Razy\Shared\this\is\root;

URL查询路由

已更改URL查询路由方式,现在它具有Lazy RouteRegex Route。您可以通过Controller->addLazyRoute()进行设置,它将自动将数组嵌套与模块代码组合作为路由,并通过键值映射文件路径。或者您可以使用Controller->addRoute()创建正则表达式以匹配URL查询,并将匹配的字符串作为参数传递。您还可以同时使用Lazy RouteRegex Route,但分发器将首先匹配Regex Route路由,然后是Lazy Route

/**
 * The module code is `Sample.Route` and the alias is `hello`
 *
 * The route `domain.com/hello/first/second` will link to ./controller/first/second.php
 * The route `domain.com/hello/root will link to ./controller/Route.root.php
 */
$this->addLazyRoute([
    'first' => [
        'second' => 'third',
    ],
    'root' => 'root'
]);

/**
 * The route `domain.com/regex/get-abc/page-1/tester` will link to ./controller/Route.regex.php
 * and it will pass the parameters `abc`, `1` and `tester` to the controller
 */
$this->addRoute('/regex/get-(:a)/page-(:d)/(:[a-z0-9_-]{3,})', 'regex');

内部跨站API

之前,当您在Razy结构下创建了多个Distributor时,API不允许直接访问Distributor,但您可以通过CURL访问另一个Distributor API,但这可能会增加执行时间。当然,将相同的功能复制到所有Distributor模块中是一个愚蠢的解决方案,但这是在每个Distributor中实现该功能的唯一方法。回到原始意图,Razy旨在提高编码管理效率和防止开发中的合并冲突。负责Module的开发者或团队应维护API以允许其他Module访问,以防止其他开发者试图修改您的代码以满足他们的需求。

在v0.4中,Razy Controller提供了一个connect()方法,允许开发者直接访问其他Distributor API。您还可以配置允许连接的Distributor白名单,或在Controller::__onAPICall()中限制访问。

$connection = $this->connect('domain.name.in.razy.com');
$connection->api('api.function', 'Developer', 'Friendly');

命名空间模块代码

您可以使用命名空间来命名您的模块代码,以防止与其他模块发生名称冲突,例如Author.Package.ClassName。您的类文件应包含位于命名空间Razy\Module\Author\Package\ClassName下的类,并且Lazy Route将从ClassName或您提供的别名开始。

/**
 * If the module code is named `Author\Sample\Route`, the class should be declared as below
 */
namespace Razy\Module\Author\Sample;

class Route
{
    // bla bla bla...
}

强制启用/禁用模块

您可以在dist.php中启用或禁用模块,这样您就不需要强制在onInit()中禁用模块。

共享模块

当您想重用模块时,无需从其他项目克隆模块,现在您可以在shared文件夹中更新模块,这样所有不在其模块文件夹中的Distributor都可以使用。

事件发射器

现在Razy有一个新的事件和监听逻辑,允许模块之间交互。在Module初始化阶段,您可以设置要监听的事件列表,例如

$this->listen('test.onload', 'pathOfMethod');

另一方面,您可以通过$this->trigger创建一个EventEmitter,通过给定的事件名称,或者传递一个额外的Closure作为handler。之后,您可以通过执行EventEmitter方法reslove(...$args)传递任意数量的参数到监听事件的模块,并将响应传递给设置好的handler。例如

$this->trigger('test.onload', function($response, $moduleCode) {
    echo $moduleCode . ' response: ' . $response;
})->resolve('hello world!');

从迭代器到集合

在v0.3中,它被称为Iternator\Manager,它是一个类似数组的数据库工厂,用于处理其元素,如trimuppercaseint。在v0.4中,它现在完全不同,甚至更强大。

为什么停止使用Iterator?这是因为PHP7.4的原生数组函数不支持在对象中使用。例如,当将ArrayObjectArrayAccess对象传递给array_key_existsarray_keys时,会提示警告消息,并且无法正常工作。因此,Razy引入了一个名为Collection的新类来替代Iterator,用于处理数组中的元素。

$sample = [
    'name' => 'Hello World',
    'path' => [
        'of' => [
            'the' => 'Road',
            'number' => 20,
            'text_a' => '    Bad Boy!',
            'text_b' => 'Good Boy!   ',
        ],    
    ],
];

$collection = collect($sample);
$result = $collection('name,path:istype("array").of.*:istype("string")')->trim()->getArray();
var_dump($result);

/**
 * The selected strings have trimmed:
 * 
 * array(4) {
 *  ["$.name"]=>
 *  string(11) "Hello World"
 *  ["$.path.of.the"]=>
 *  string(4) "Road"
 *  ["$.path.of.text_a"]=>
 *  string(8) "Bad Boy!"
 *  ["$.path.of.text_b"]=>
 *  string(9) "Good Boy!"
 * }
 */

如上所示,示例显示了您可以使用选择器语法name,path:istype("array").of.*:istype("string")来匹配由Collection收集的元素。语法类似于CSS选择器。此外,您还可以使用以冒号开头的模式,例如:plugin(paramA, paramB)来过滤通过插件函数实现的测试匹配的元素。

在选择器解析后,匹配的元素将传递给Processor进行进一步处理,例如通过插件函数实现的trimupperlower,或者调用get()返回一个新的包含匹配值的Collection对象。

优化后的模板引擎

Razy增强了模板引擎,可以很好地解析参数标签和字符串值。此外,参数的关闭标签已移除,虽然在模板文件中它提供了一个相同的哈希标签,但仍然非常令人困惑且难以识别。

模板引擎在v0.3版本中运行平稳且成熟,因此在结构或格式上没有太大差异。关于插件修饰符和函数,它有相当大的不同。首先,修饰符格式已更改以满足缩短的条件语法。

在v0.3

{$parameter.path.of.the.value|mod:"param":"here"|othermod}

现在在v0.4

{$parameter.path.of.the.value->mod:"param":"here"->othermod}

因此,我们可以在if函数标签中使用修饰符语法参数!

{@if $text|$parameter.path.of.the.value->gettype="array"}
// blah blah blah
{/if}

如您在上面的更改中看到,修饰符分隔符已从|更改为->,这与PHP方法调用相似。其次,函数标签也可以配置为它是一个块语句的封装或独立标签,因此更容易进行插件编码。

最后,参数标签最终支持修饰符语法作为函数标签的参数,并且参数将作为从Entity参数解析的值传递给处理器,这样我们就不需要在处理器中解析参数。

注意,一些函数插件已更新以满足上述更改,并且函数标签支持配置为3种格式,即ShortenParameter SetBypass

在v0.3

{@each source=$arraydata key="key" value="value"}
Key: {$key}
Value: {$value}
{/each}

在v0.4

// Shorten, ordered by source, kvp
{@each $arraydata}
Key: {$kvp.key}
Value: {$key.value}
{/each}

// Parameter Set
{@each source=$arraydata kvp="nameofkvp"}
Key: {$nameofkvp.key}
Value: {$nameofkey.value}
{/each}

// Bypass
{@if $data->gettype="array",($data.value="hello"|$data.value="world")}
// The content after `if` will pass to the plugin as the first parameter
{/if}

传统的参数声明方式已弃用,它已被函数标签def所取代。

在v0.3

{$name: "Define a new variable"}

在v0.4

{@def "name" "Define a new variable"}

// Or you can copy the value from other variable
{@def "newvalue" $data.path.of.value}

因此,v0.4还添加了3种不同的模板块类型,即INCLUDETEMPLATEUSE。这对于加载外部模板文件或在任何子块中重用模板块非常有用。

<!-- START BLOCK: blockA -->
    <!-- TEMPLATE BLOCK: template -->
    Here is the template content
    <!-- END BLOCK: template -->
    
    <!-- START BLOCK: sample -->
        Below is the content generated from the TEMPLATE block
        <!-- USE template BLOCK: subblock -->
    <!-- END BLOCK: sample -->
<!-- END BLOCK: blockA -->

<!-- START BLOCK: blockB -->
    Include the external template file from the current file location
    <!-- INCLUDE BLOCK: folder/external.tpl -->
    You cannot use the template block from other block!
    <!-- USE template BLOCK: subblock -->
<!-- END BLOCK: blockB -->

最后要说的是,模板引擎已重新编写代码以使用生成器获取模板文件的内容,因为它将节省大量内存,因为Razy之前会加载所有文件内容到内存中。

数据库语句简单语法

在v0.3版本中,Razy的WhereSyntax和TableJoinSyntax提供了清晰且简短的语法来生成MySQL语句。这对于维护复杂的MySQL语句非常有帮助,同时它还可以通过简单的运算符,如~=:=,生成多个MySQL JSON_*函数组合。在v0.4版本中,Razy增强了TableJoinSyntax和WhereSyntax,使其解析语法更准确,防止用户提供无效的语法格式生成不完整或无效的语句。此外,WhereSyntax也增强了运算符的解析准确性,它会检测运算符的类型以生成不同的语句。

$statement = $database->prepare()->from('u.user-g.group[group_id]')->where('u.user_id=?,!g.auths~=?')->assign([
    'auths' => 'view',
    'user_id' => 1,
]);
echo $statement->getSyntax();

/**
 * Result:
 * SELECT * FROM `user` AS `u` JOIN `group` AS `g` ON u.group_id = g.group_id WHERE `u`.`user_id` = 1 AND !(JSON_CONTAINS(JSON_EXTRACT(`g`.`auths`, '$.*'), '"view"') = 1)
 */
 
运算符 描述
= 等于
|= 列表中搜索
*= 包含字符串
^= 以字符串开头
$= 以字符串结尾
!= 不等于
< 小于
> 大于
<= 小于等于
>= 大于等于
:= 通过给定路径提取指定列中JSON数据类型的节点
~= 在指定列中搜索JSON数据类型的值或值列表
&= 在指定列中搜索JSON数据类型的字符串
@= 在指定列的JSON数据类型中匹配多个键

Razy简单的表连接和Where语句语法在编写复杂且长的语句时提供了很大优势,但它不足以覆盖大多数语句。实际上,我使用Razy v0.3开发了几个系统,遇到了一个关键问题,那就是Database\Statement::update函数太简单,无法涵盖一些有用的简单语句,如递增或递减、连接或数学运算符。因此,Razy v0.4提供了简单的Update Statement语法,且v0.3和v0.4之间没有函数使用变化。

v0.3

echo $database->update('table_name', ['comment', 'name'])->where('id=1')->assign([
    'comment' => 'Hello World',
    'name' => 'Razy',
])->getSyntax();

/**
 * Result:
 * UPDATE table_name SET `comment` = 'Hello World', `name` = 'Razy' WHERE `id` = 1;
 */

v0.4

echo $database->update('table_name', ['name', 'count++', 'document_code="doc_"&id', 'path&=?', 'another_count+=4'])->where('id=1')->assign([
    'name' => 'Razy',
    'path' => '/node',
])->getSyntax();

/**
 * Result:
 * UPDATE table_name SET `name` = 'Razy', `count` = `count` + 1, `document_code` = CONCAT("doc_", id), `path` = CONCAT(`path`, '/node'), `another_count` = `another_count` + 4 WHERE `id` = 1;
 */

数据库表和列

在v0.3中,开发者可以使用Database::TableDatabase::Column类来创建表和列,生成SQL语句,但当升级模块时,修改或添加列或表很困难。因此,在v0.4中,Database::TableDatabase::Column已增强以支持修改表和列,它将从每个Database::Table->commit()生成SQL语句。

此外,Database::TableDatabase::Column还支持将配置语法作为参数传递,用于导入所有表设置及其列设置。这对于提交先前版本的表设置并生成更新表的SQL语句非常有用。

在v0.3

// Create a Table
$table = new Database\Table('test_table');

// Create a new column, and set the type as an auto increment id.
$columnA = $table->addColumn('column_a');
$columnA->type('auto');

// Create a new column, and set the type as int, length 11 and default value to 1
$columnB = $table->addColumn('column_b');
$columnB->type('int')->length('11')->default('1');

// Generate the create table syntax
echo $table->getSyntax();
/**
 * Result:
 * CREATE TABLE test_table (`column_a` INT(8) NOT NULL AUTO_INCREMENT, `column_b` INT(11) NOT NULL DEFAULT '0', `column_c` TINYINT(1) NOT NULL DEFAULT '0', PRIMARY KEY(`column_a`)) ENGINE InnoDB CHARSET=utf8 COLLATE utf8mb4_general_ci;
 */

在v0.4

// Create a Table
$table = new Database\Table('test_table');
$columnA = $table->addColumn('column_a=type(auto)');
$columnB = $table->addColumn('column_b=type(int),length(11),default(1)');
$columnC = $table->addColumn('column_c')->setType('bool');

// Generate Create Table SQL Statement in first commit.
echo $table->commit();
/**
 * Result:
 * CREATE TABLE test_table (`column_a` INT(8) NOT NULL AUTO_INCREMENT, `column_b` INT(11) NOT NULL DEFAULT '0', `column_c` TINYINT(1) NOT NULL DEFAULT '0', PRIMARY KEY(`column_a`)) ENGINE InnoDB CHARSET=utf8 COLLATE utf8mb4_general_ci;
 */

// Reorder the columnC and modify the columnB type
$table->moveColumnAfter('column_c', 'column_a');
$columnB->setType('text');

// Generate Alter Table and Alter Column Statement
echo $table->commit();

/**
 * Result:
 * ALTER TABLE `test_table`, MODIFY COLUMN column_c TINYINT(1) NOT NULL AFTER column_a, MODIFY COLUMN column_b VARCHAR(255) NOT NULL DEFAULT '';
 */

导入和导出配置

$table = Database\Table::Import('`table_name`=charset(utf8),collation(utf8_general_ci)[`column_b`=type(int),length(11),default("0"):`column_a`=type(auto),length(8),default("0")]');

// Add extra column
$table->addColumn('text_column');
echo $table->exportConfig();
/**
 * `table_name`=charset(utf8),collation(utf8_general_ci)[`column_b`=type(int),length(11),default("0"):`column_a`=type(auto),length(8),default("0"):`text_column`=type(text),length(255)]
 */

增强异常处理

在v0.3中,异常处理程序不够准确,无法找到异常位置,因此Razy v0.4重写了所有库异常处理,并抛出带有正确文件和行的异常。以前,可能会堆叠额外的堆栈跟踪,从而更难追踪错误,因此这一新变化将帮助开发者更快、更容易地调试。

自动加载器

在v0.3中,Razy支持在library文件夹下自动加载类,但命名空间不是按Psr标准命名的,因此Razy v0.4进行了重大更改。首先,所有Razy核心类都已移动到library下的Razy文件夹。

其次,Razy有2层自动加载阶段,Razy结构的根和Razy.phar。Razy首先在Razy结构根下搜索类文件,然后是Razy.phar。这对于在不覆盖原始类的情况下覆盖Razy核心类,或为您的项目创建新类非常有用。

听起来不错吗?更重要的是,Razy v0.4 支持从 packagist.org 安装或更新包。是的!与 composer 仓库集成可以帮助开发者更容易地管理包,无需痛苦地加载项目中类的代码。此外,来自 packagist.org 的类将由发行商在 autoload 文件夹下分开,以防止版本冲突。

使用以下命令更新或安装模块和 composer 包

php Razy.phar validate distCode

最后,Razy v0.4 也更改了 Module 库的自动加载逻辑,现在 Module 库中的类应该位于 Module 命名空间下

/**
 * The ABC Module namespace
 */
namespace Razy\Module\distCode\ABC;
namespace Razy\Shared\ABC;

/**
 * The Sample classes under the ABC module
 */
namespace Razy\Module\distCode\ABC\Sample;
namespace Razy\Shared\ABC\Sample;

上述更改完全隔离了 composer 包自定义类发行商类模块类,以防止任何不匹配的版本类冲突、类名冲突、混乱和编码管理混乱。

此外,Razy v0.4 现在支持 Psr-0 自动加载。

开放/关闭原则,SOLID 设计

以下表格显示了每个类之间的注入和路径。

核心

应用 领域 发行商 模块 控制器 *
connect()
trigger()
api()
handshake()
addRoute() ←*
addLazyRoute() ←*
addAPI() ←*
listen() ←*
getAPI()
query()
  • 有一个 Pilot 对象用于在 __onInit() 阶段配置路由、API 或事件。当调用 Controller 方法时,Module 将将闭包绑定到 Controller 继承的对象,而不是 Controller 的抽象类。它可以防止继承的 Controller 对象包含其闭包来访问抽象类中的私有方法或属性。

控制器事件

事件 描述
__onInit(): bool 模块加载后触发,返回 false 以标记模块为未加载
__onReady(): void 当所有模块都加载完毕时,API 和事件将启用,并且所有模块将触发一次。
__onRoute(): bool 仅在模块路由与 URL 查询匹配时触发,返回 false 以拒绝匹配。
__onAPICall(): bool 仅在调用模块的 API 时触发,返回 false 以拒绝 API。
__onTrigger(): bool 仅在调用模块的监听事件时触发,返回 false 以拒绝事件触发。
__onError(): void 仅在模块抛出任何错误时触发。