教程:Hyperf
根据之前文章:hyperf 十九 数据库 二 模型-CSDN博客?? 应该能了解到visitors参数。
根据教程,使用visitors参数创建脚本。在配置在设置visitors参数,格式为数据。
$incrementing
$primaryKey
和 $keyType
。DELETED_AT
常量判断该模型是否含有软删除字段,如果存在,则添加对应的 Trait SoftDeletes。
created_at
和 updated_at
自动判断,是否启用默认记录 创建和修改时间
的功能。getter
和 setter
。教程中作为例子的被覆盖的脚本为Hyperf\Database\Commands\Ast\ModelUpdateVisitor。
Hyperf\Database\Commands\ModelCommand中默认使用的脚本包括Hyperf\Database\Commands\Ast\ModelUpdateVisitor、Hyperf\Database\Commands\Ast\ModelRewriteConnectionVisitor。
ModelCommand中通过?PhpParser\NodeTraverser类调用脚本。对NodeTraverser类设置脚本,NodeTraverser::traverse()循环被添加的脚本处理节点数据。ModelCommand获取处理后的数据设置为文件内容。
如教程上所示,自定义脚本中覆盖部分方法,再对应类。再次运行应该会执行自定义的内容。
namespace App\Kernel\Visitor;
use Hyperf\Database\Commands\Ast\ModelUpdateVisitor as Visitor;
use Hyperf\Utils\Str;
class ModelUpdateVisitor extends Visitor
{
/**
* Used by `casts` attribute.
*/
protected function formatDatabaseType(string $type): ?string
{
switch ($type) {
case 'tinyint':
case 'smallint':
case 'mediumint':
case 'int':
case 'bigint':
return 'integer';
case 'decimal':
// 设置为 decimal,并设置对应精度
return 'decimal:2';
case 'float':
case 'double':
case 'real':
return 'float';
case 'bool':
case 'boolean':
return 'boolean';
default:
return null;
}
}
/**
* Used by `@property` docs.
*/
protected function formatPropertyType(string $type, ?string $cast): ?string
{
if (! isset($cast)) {
$cast = $this->formatDatabaseType($type) ?? 'string';
}
switch ($cast) {
case 'integer':
return 'int';
case 'date':
case 'datetime':
return '\Carbon\Carbon';
case 'json':
return 'array';
}
if (Str::startsWith($cast, 'decimal')) {
// 如果 cast 为 decimal,则 @property 改为 string
return 'string';
}
return $cast;
}
}
#config/autoload/dependencies.php
return [
Hyperf\Database\Commands\Ast\ModelUpdateVisitor::class => App\Kernel\Visitor\ModelUpdateVisitor::class,
];
配置
#config/autoload/database.php
'commands' => [
'gen:model' => [
'path' => '/app1/Model',
'force_casts' => true,
'inheritance' => 'Model',
'visitors' => [
'Hyperf\Database\Commands\Ast\ModelRewriteKeyInfoVisitor',
'Hyperf\Database\Commands\Ast\ModelRewriteTimestampsVisitor',
'Hyperf\Database\Commands\Ast\ModelRewriteSoftDeletesVisitor',
],
'table_mapping' => ['userinfo:User'],
],
],
这里需要注意的是gen:model里面的键名,比如命令为table-mapping,但是设置的时候键名为table_mapping。造成这个现象,是因为框架里获取用的键名与命令中参数名不一致。
执行命令:php bin/hyperf.php gen:model? userinfo
生成文件
declare (strict_types=1);
namespace App1\Model;
use Hyperf\Database\Model\SoftDeletes;
use Hyperf\DbConnection\Model\Model;
/**
* @property int $id
* @property string $name
* @property int $age
* @property string $deleted_at
*/
class User extends Model
{
use SoftDeletes;
public $timestamps = false;
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'userinfo';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [];
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = ['id' => 'integer', 'age' => 'integer'];
}
查询
#测试代码
$user = User::query()->where('id', 1)->first();
var_dump($user->name, $user->age, $user->toArray());
#运行结果
string(3) "123"
int(22)
array(4) {
["id"]=>
int(1)
["name"]=>
string(3) "123"
["age"]=>
int(22)
["deleted_at"]=>
NULL
}
根据源码Hyperf\Database\Commands\Ast\ModelRewriteSoftDeletesVisitor::useSoftDeletes(),
DELETED_AT是动态设置,会判断model中是否有DELETED_AT没有才会设置为deleted_at。
自定义model的父类Hyperf\Database\Model\Model没有设置DELETED_AT,所以要修改DELETED_AT对应的数据库名,而且在不改源码的基础上,需要在已创建的model中设置DELETED_AT。
#测试代码
namespace App1\Model;
use Hyperf\Database\Model\SoftDeletes;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
use SoftDeletes;
public const DELETED_AT = 'deleted_time';
}
#测试结果
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'userinfo.deleted_time' in 'where clause' (SQL: update `userinfo` set `deleted_time` = 2024-01-05 08:23:02 where (`id` = 23) and `userinfo`.`deleted_time` is null)[1088] in /wj/hyperf/hyperfpro2/vendor/hyperf/database/src/Connection.php
?报错是因为没改数据库,所以没有对应字段。虽然报错,但是证明sql执行正常,所以测试成功。
CREATED_AT,UPDATED_AT修改方案和上面相同。
#Hyperf\Database\Commands\ModelCommand
public function handle()
{
$table = $this->input->getArgument('table');
$pool = $this->input->getOption('pool');
$option = new ModelOption();
$option->setPool($pool)
->setPath($this->getOption('path', 'commands.gen:model.path', $pool, 'app/Model'))
->setPrefix($this->getOption('prefix', 'prefix', $pool, ''))
->setInheritance($this->getOption('inheritance', 'commands.gen:model.inheritance', $pool, 'Model'))
->setUses($this->getOption('uses', 'commands.gen:model.uses', $pool, 'Hyperf\DbConnection\Model\Model'))
->setForceCasts($this->getOption('force-casts', 'commands.gen:model.force_casts', $pool, false))
->setRefreshFillable($this->getOption('refresh-fillable', 'commands.gen:model.refresh_fillable', $pool, false))
->setTableMapping($this->getOption('table-mapping', 'commands.gen:model.table_mapping', $pool, []))
->setIgnoreTables($this->getOption('ignore-tables', 'commands.gen:model.ignore_tables', $pool, []))
->setWithComments($this->getOption('with-comments', 'commands.gen:model.with_comments', $pool, false))
->setWithIde($this->getOption('with-ide', 'commands.gen:model.with_ide', $pool, false))
->setVisitors($this->getOption('visitors', 'commands.gen:model.visitors', $pool, []))
->setPropertyCase($this->getOption('property-case', 'commands.gen:model.property_case', $pool));
if ($table) {
$this->createModel($table, $option);
} else {
$this->createModels($option);
}
}
protected function getOption(string $name, string $key, string $pool = 'default', $default = null)
{
$result = $this->input->getOption($name);
$nonInput = null;
if (in_array($name, ['force-casts', 'refresh-fillable', 'with-comments', 'with-ide'])) {
$nonInput = false;
}
if (in_array($name, ['table-mapping', 'ignore-tables', 'visitors'])) {
$nonInput = [];
}
if ($result === $nonInput) {
$result = $this->config->get("databases.{$pool}.{$key}", $default);
}
return $result;
}
#Hyperf\Config\ConfigProvider
'dependencies' => [
ConfigInterface::class => ConfigFactory::class,
],
namespace Hyperf\Config;
class ConfigFactory
{
public function __invoke(ContainerInterface $container)
{
$configPath = BASE_PATH . '/config/';
$config = $this->readConfig($configPath . 'config.php');
$autoloadConfig = $this->readPaths([BASE_PATH . '/config/autoload']);
$merged = array_merge_recursive(ProviderConfig::load(), $config, ...$autoloadConfig);
return new Config($merged);
}
}
namespace Hyperf\Config;
class Config implements ConfigInterface
{
/**
* @var array
*/
private $configs = [];
public function __construct(array $configs)
{
$this->configs = $configs;
}
public function get(string $key, $default = null)
{
return data_get($this->configs, $key, $default);
}
}
#vendor\hyperf\utils\src\Functions.php
if (!function_exists('data_get')) {
/**
* Get an item from an array or object using "dot" notation.
*
* @param null|array|int|string $key
* @param null|mixed $default
* @param mixed $target
*/
function data_get($target, $key, $default = null)
{
//var_dump("data_get");
if (is_null($key)) {
return $target;
}
//var_dump($target, $key);
$key = is_array($key) ? $key : explode('.', is_int($key) ? (string) $key : $key);
while (!is_null($segment = array_shift($key))) {
//var_dump($segment);
if ($segment === '*') {
if ($target instanceof Collection) {
$target = $target->all();
} elseif (!is_array($target)) {
return value($default);
}
$result = [];
foreach ($target as $item) {
$result[] = data_get($item, $key);
}
return in_array('*', $key) ? Arr::collapse($result) : $result;
}
if (Arr::accessible($target) && Arr::exists($target, $segment)) {
$target = $target[$segment];
} elseif (is_object($target) && isset($target->{$segment})) {
$target = $target->{$segment};
} else {
return value($default);
}
}
return $target;
}
}
#Hyperf\Database\Model\Model
public const CREATED_AT = 'created_at';
public const UPDATED_AT = 'updated_at';
#Hyperf\Database\Commands\Ast\ModelRewriteSoftDeletesVisitor
public function afterTraverse(array $nodes)
{
foreach ($nodes as $namespace) {
if (! $namespace instanceof Node\Stmt\Namespace_) {
continue;
}
if (! $this->hasSoftDeletesUse && ($newUse = $this->rewriteSoftDeletesUse())) {
array_unshift($namespace->stmts, $newUse);
}
foreach ($namespace->stmts as $class) {
if (! $class instanceof Node\Stmt\Class_) {
continue;
}
if (! $this->hasSoftDeletesTraitUse && ($newTraitUse = $this->rewriteSoftDeletesTraitUse())) {
array_unshift($class->stmts, $newTraitUse);
}
}
}
}
protected function rewriteSoftDeletesUse(?Node\Stmt\Use_ $node = null): ?Node\Stmt\Use_
{
if ($this->shouldRemovedSoftDeletes()) {
return null;
}
if (is_null($node)) {
$use = new Node\Stmt\UseUse(new Node\Name(SoftDeletes::class));
$node = new Node\Stmt\Use_([$use]);
}
return $node;
}
protected function rewriteSoftDeletesTraitUse(?Node\Stmt\TraitUse $node = null): ?Node\Stmt\TraitUse
{
if ($this->shouldRemovedSoftDeletes()) {
return null;
}
if (is_null($node)) {
$node = new Node\Stmt\TraitUse([new Node\Name('SoftDeletes')]);
}
return $node;
}
protected function shouldRemovedSoftDeletes(): bool
{
$useSoftDeletes = $this->useSoftDeletes();
$ref = new \ReflectionClass($this->data->getClass());
if (! $ref->getParentClass()) {
return false;
}
return $useSoftDeletes == $ref->getParentClass()->hasMethod('getDeletedAtColumn');
}
protected function useSoftDeletes(): bool
{
$model = $this->data->getClass();
$deletedAt = defined("{$model}::DELETED_AT") ? $model::DELETED_AT : 'deleted_at';
return Collection::make($this->data->getColumns())->where('column_name', $deletedAt)->count() > 0;
}
#PhpParser\NodeTraverser
public function traverse(array $nodes) : array {
$this->stopTraversal = false;
foreach ($this->visitors as $visitor) {
if (null !== $return = $visitor->beforeTraverse($nodes)) {
$nodes = $return;
}
}
$nodes = $this->traverseArray($nodes);
foreach ($this->visitors as $visitor) {
if (null !== $return = $visitor->afterTraverse($nodes)) {
$nodes = $return;
}
}
return $nodes;
}
#Hyperf\Database\Commands\ModelCommand
protected function createModel(string $table, ModelOption $option)
{
$builder = $this->getSchemaBuilder($option->getPool());
$table = Str::replaceFirst($option->getPrefix(), '', $table);
$columns = $this->formatColumns($builder->getColumnTypeListing($table));
$project = new Project();
$class = $option->getTableMapping()[$table] ?? Str::studly(Str::singular($table));
$class = $project->namespace($option->getPath()) . $class;
$path = BASE_PATH . '/' . $project->path($class);
if (!file_exists($path)) {
$this->mkdir($path);
file_put_contents($path, $this->buildClass($table, $class, $option));
}
$columns = $this->getColumns($class, $columns, $option->isForceCasts());
$stms = $this->astParser->parse(file_get_contents($path));
$traverser = new NodeTraverser();
$traverser->addVisitor(make(ModelUpdateVisitor::class, [
'class' => $class,
'columns' => $columns,
'option' => $option,
]));
$traverser->addVisitor(make(ModelRewriteConnectionVisitor::class, [$class, $option->getPool()]));
$data = make(ModelData::class)->setClass($class)->setColumns($columns);
foreach ($option->getVisitors() as $visitorClass) {
$traverser->addVisitor(make($visitorClass, [$option, $data]));
}
$stms = $traverser->traverse($stms);
$code = $this->printer->prettyPrintFile($stms);
file_put_contents($path, $code);
$this->output->writeln(sprintf('<info>Model %s was created.</info>', $class));
if ($option->isWithIde()) {
$this->generateIDE($code, $option, $data);
}
}