hyperf 二十 数据库 三 创建脚本

发布时间:2024年01月05日

教程:Hyperf

根据之前文章:hyperf 十九 数据库 二 模型-CSDN博客?? 应该能了解到visitors参数。

根据教程,使用visitors参数创建脚本。在配置在设置visitors参数,格式为数据。

一、可选脚本说明

  • Hyperf\Database\Commands\Ast\ModelRewriteKeyInfoVisitor:根据数据库中主键,生成对应的 $incrementing $primaryKey$keyType
  • Hyperf\Database\Commands\Ast\ModelRewriteSoftDeletesVisitor:据 DELETED_AT 常量判断该模型是否含有软删除字段,如果存在,则添加对应的 Trait SoftDeletes。
  • Hyperf\Database\Commands\Ast\ModelRewriteTimestampsVisitor:根据 created_atupdated_at 自动判断,是否启用默认记录 创建和修改时间 的功能。
  • Hyperf\Database\Commands\Ast\ModelRewriteGetterSetterVisitor:根据数据库字段生成对应的 gettersetter

二、自定义映射关系

教程中作为例子的被覆盖的脚本为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
}

?四、测试 修改DELETED_AT

根据源码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修改方案和上面相同。

五、源码

4.1 参数值获取

#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;
    }
}

4.2 DELETED_AT、CREATED_AT、UPDATED_AT相关

#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);
        }
    }

文章来源:https://blog.csdn.net/lsswear/article/details/135382232
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。