rayfunghk / razy
一个用于Web开发的PHP框架,具有高性能和灵活性。非常适合团队开发和代码维护。
Requires
- php: ^7.4 || ^8.0
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资产(如css
、js
或image
)位于它们自己的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', ], ];
上述列表中的资产将通过set
、remove
、link
、unlink
、unpackasset
和fix
命令从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 Route
和Regex Route
。您可以通过Controller->addLazyRoute()
进行设置,它将自动将数组嵌套与模块代码组合作为路由,并通过键值映射文件路径。或者您可以使用Controller->addRoute()
创建正则表达式以匹配URL查询,并将匹配的字符串作为参数传递。您还可以同时使用Lazy Route
和Regex 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
,它是一个类似数组的数据库工厂,用于处理其元素,如trim
、uppercase
或int
。在v0.4中,它现在完全不同,甚至更强大。
为什么停止使用Iterator
?这是因为PHP7.4的原生数组函数不支持在对象中使用。例如,当将ArrayObject
或ArrayAccess
对象传递给array_key_exists
或array_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
进行进一步处理,例如通过插件函数实现的trim
、upper
或lower
,或者调用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种格式,即Shorten
、Parameter Set
和Bypass
。
在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种不同的模板块类型,即INCLUDE
、TEMPLATE
和USE
。这对于加载外部模板文件或在任何子块中重用模板块非常有用。
<!-- 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::Table
和Database::Column
类来创建表和列,生成SQL语句,但当升级模块时,修改或添加列或表很困难。因此,在v0.4中,Database::Table
和Database::Column
已增强以支持修改表和列,它将从每个Database::Table->commit()
生成SQL语句。
此外,Database::Table
和Database::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 | 仅在模块抛出任何错误时触发。 |