hyperf 十九 数据库 二 模型

教程:Hyperf

一、命令行

symfony/console-CSDN博客

hypery 十一、命令行-CSDN博客

hyperf console 执行-CSDN博客

根据之前应该能了解到命令行的基本实现,和hyperf中命令行的定义。

1.1 命令初始化

hyperf.php中系统初始化中通过ApplicationFactory::__invoke(),将配置文件中的commands对应内容,通过Application::add($container->get($command)),设置为Application::command参数的值,类型为数组。

以ModelCommand::configure()为例,通过Hyperf\Config\ProviderConfig::load()进行配置加载,其作用就是将ModelCommand之类的命令类实例化,并设置为ProviderConfig::providerConfigs的值。

实例化的过程中,调用顺序为:

        1、Hyperf\Database\Commands\ModelCommand::__construct()

        2、Hyperf\Command\Command::__construct()

        3、Symfony\Component\Console\Command::__construct()

        4、$this->configure();

 

1.2 调用命令

执行命令通过Symfony\Component\Console\Application::run(),其中总过Application::get()获取Application::command中对应命令的尸体类。

Application::run()会调用Application::doRunCommand(),doRunCommand()中执行对应命令的run()方法。

实际运行以ModelCommand::handle()为例,调用顺序为:

        1、Hyperf\Database\Commands\ModelCommand::run()

        2、Hyperf\Command\Command::run()

        3、Symfony\Component\Console\Command\Command::run()

        4、Hyperf\Command\Command::execute()

        5、Hyperf\Database\Commands\ModelCommand::handle()

1.3 ModelCommand执行

命令执行的入口为ModelCommand::handle(),通过设置参数,获取可处理数据。

若设置table则执行createModel(),否者执行createModels()。createModels()通过循环调用createModel()。

参数中ignore-tables,可设置需要忽略的表,在createModels()中起作用。

参数pool、inheritance、uses,替换对应/stubs/Model.stub文件中的内容。

参数path、prefix、table-mapping,则影响model文件生成。

和文档中不同的参数包括:with-ide、visitors

with-ide:是否生成对应mode的ide文件

visitors:设置ModelUpdateVisitor、ModelRewriteConnectionVisitor之类,用于处理数据的类。可用文件在vendor\hyperf\database\src\Commands\Ast中。可参考:创建脚本 - Hyperf 帮助文档 v2.0 - 开发文档 - 文江博客

二、参数设置

命令行

php bin/hyperf.php gen:model table_name

参数

参数 类型 默认值 备注
--pool string default 连接池,脚本会根据当前连接池配置创建
--path string app/Model 模型路径
--force-casts bool false 是否强制重置 casts 参数
--prefix string 空字符串 表前缀
--inheritance string Model 父类
--uses string Hyperf\DbConnection\Model\Model 配合 inheritance 使用
--refresh-fillable bool false 是否刷新 fillable 参数
--table-mapping array [] 为表名 -> 模型增加映射关系 比如 ['users:Account']
--ignore-tables array [] 不需要生成模型的表名 比如 ['users']
--with-comments bool false 是否增加字段注释
--property-case int 0 字段类型 0 蛇形 1 驼峰

可以通过命令行设置参数,可也在设置中写入参数。

设置中参数设置:

#config\autoload\databases.php
use Hyperf\Database\Commands\ModelOption;

return [
    'default' => [
        // 忽略其他配置
        'commands' => [
            'gen:model' => [
                'path' => 'app/Model',
                'force_casts' => true,
                'inheritance' => 'Model',
                'uses' => '',
                'refresh_fillable' => true,
                'table_mapping' => [],
                'with_comments' => true,
                'property_case' => ModelOption::PROPERTY_SNAKE_CASE,
            ],
        ],
    ],
];

 命令中设置参数的时候,通过ModelCommand::getOption(),会将命令行ArgvInput()::getOption(),通过判断是否使用配置中的参数

根据代码,参数中force-casts、refresh-fillable、with-comments、with-ide,若命令行中未传且配置中已设置,则会使用配置中的值。参数中table-mapping、ignore-tables、visitors都为数组,若命令行中未传且配置中已设置,则会使用配置中的值。即配置参数以命令行中参数优先使用

三、测试

3.1 配置

hyperf.php配置

 'commands' => [
      'gen:model' => [
      'path' => 'app1/Model',
      'force_casts' => true,
      'inheritance' => 'Model',
   ],
],

 mysql:

CREATE TABLE `userinfo` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `age` tinyint(2) DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=22 DEFAULT CHARSET=utf8;

 composer.json

 "autoload": {
        "psr-4": {
            "App\\": "app/",
            "App1\\":"app1/"
        },
        "files": []
    },

3.2 创建

php bin/hyperf.php gen:model --table-mapping='userinfo:User' --prefix=app1 userinfo

执行挺简单一句, 没想到坑还挺多。

首先因为使用的是app1,而非app,所以在composer.json中增加psr-4,因为框架会循环psr-4中的值,根据逻辑判断返回对应的$path,没有匹配的数据则抛出throw。

composer.json修改之后需要composer update。否则对于以上情况,第一次生成model之后,再次处理对应model会报model找不到。

最后发现的一个坑,是文档上写明table-mapping应该是数组,写法应该是["userinfo:User"]。但是特别奇葩的是,创建过程中,将值格式化为["[userinfo"=>"User]"]……后来发现,table-mapping获取的值都是字符串和输入的格式无关,但是之后getTableMapping()没有对字符串进行处理,仅是分割字符串。可能和版本有关,或者因为我输入的格式有误。

还有一个可能是编译器导致的错误,创建的时候有个框架里的文件报错,里面的使用的部分类找不到。原因是没有设置对应的use,但是框架里对应文件存在,补充use语句之后bug解决。

3.3 插入

#model
class User extends Model
{
    public $timestamps = false;
}

#测试
class TestController extends AbstractController
{
    public function testmodel()
    {
        $name = $this->request->input('name');
        $m_u = new User();
        $m_u->name = (string) $name;
        $result = $m_u->save();
        var_dump($result);
    }
}

#执行结果
true

数据库配置文件在config/autoload/databases.php,但是里面设置的默认值只有在.env中对应值未设置的时候才生效。所以已有.env时,应该再检查下该文件数据库配置是否有误。

默认使用created_at,updated_at字段,设置$timestamps = false后关闭。

3.4 查询

$user = User::query()->where('id', 1)->first();
var_dump($user->name, $user->age);

#运行结果
string(3) "123"
int(22)

3.5 软删除

#model
use Hyperf\DbConnection\Model\Model;
use Hyperf\Database\Model\SoftDeletes;
/**
 */
class User extends Model
{
    use SoftDeletes;
}

#测试代码
$result = User::query()->where(['id' => 23])->delete();
var_dump($result);

#测试结果
int(1)

 软删除必须在数据中设置deleted_at字段。

四、源码

4.1 ModelCommand执行

namespace Hyperf\Database\Commands;
class ModelCommand extends Command
{
    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 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('Model %s was created.', $class));

        if ($option->isWithIde()) {
            $this->generateIDE($code, $option, $data);
        }
    }
protected function createModels(ModelOption $option)
    {
        $builder = $this->getSchemaBuilder($option->getPool());
        $tables = [];

        foreach ($builder->getAllTables() as $row) {
            $row = (array) $row;
            $table = reset($row);
            if (!$this->isIgnoreTable($table, $option)) {
                $tables[] = $table;
            }
        }

        foreach ($tables as $table) {
            $this->createModel($table, $option);
        }
    }
    protected function isIgnoreTable(string $table, ModelOption $option): bool
    {
        if (in_array($table, $option->getIgnoreTables())) {
            return true;
        }

        return $table === $this->config->get('databases.migrations', 'migrations');
    }
}
namespace Hyperf\Utils\CodeGen;
class Project
{
    public function namespace(string $path): string
    {
        $ext = pathinfo($path, PATHINFO_EXTENSION);

        if ($ext !== '') {
            $path = substr($path, 0, -(strlen($ext) + 1));
        } else {
            $path = trim($path, '/') . '/';
        }

        foreach ($this->getAutoloadRules() as $prefix => $prefixPath) {
            if ($this->isRootNamespace($prefix) || strpos($path, $prefixPath) === 0) {
                return $prefix . str_replace('/', '\\', substr($path, strlen($prefixPath)));
            }
        }
        throw new \RuntimeException("Invalid project path: {$path}");
    }
     protected function getAutoloadRules(): array
    {
        return data_get(Composer::getJsonContent(), 'autoload.psr-4', []);
    }
}
namespace Hyperf\Database\Commands;
class ModelOption
{
     public function setTableMapping(array $tableMapping): self
    {
        foreach ($tableMapping as $item) {
            [$key, $name] = explode(':', $item);
            $this->tableMapping[$key] = $name;
        }

        return $this;
    }
}

4.2 $timestamps使用

namespace Hyperf\Database\Model;
abstract class Model implements ArrayAccess, Arrayable, Jsonable, JsonSerializable, CompressInterface
{
 use Concerns\HasTimestamps;
 public function save(array $options = []): bool
    {
        $this->mergeAttributesFromClassCasts();

        $query = $this->newModelQuery();

        // If the "saving" event returns false we'll bail out of the save and return
        // false, indicating that the save failed. This provides a chance for any
        // listeners to cancel save operations if validations fail or whatever.
        if ($saving = $this->fireModelEvent('saving')) {
            if ($saving instanceof StoppableEventInterface && $saving->isPropagationStopped()) {
                return false;
            }
        }

        // If the model already exists in the database we can just update our record
        // that is already in this database using the current IDs in this "where"
        // clause to only update this model. Otherwise, we'll just insert them.
        if ($this->exists) {
            $saved = $this->isDirty() ? $this->performUpdate($query) : true;
        } else {
            // If the model is brand new, we'll insert it into our database and set the
            // ID attribute on the model to the value of the newly inserted row's ID
            // which is typically an auto-increment value managed by the database.
            $saved = $this->performInsert($query);

            if (! $this->getConnectionName() && $connection = $query->getConnection()) {
                $this->setConnection($connection->getName());
            }
        }

        // If the model is successfully saved, we need to do a few more things once
        // that is done. We will call the "saved" method here to run any actions
        // we need to happen after a model gets successfully saved right here.
        if ($saved) {
            $this->finishSave($options);
        }

        return $saved;
    }
     protected function performUpdate(Builder $query)
    {
        // If the updating event returns false, we will cancel the update operation so
        // developers can hook Validation systems into their models and cancel this
        // operation if the model does not pass validation. Otherwise, we update.
        if ($event = $this->fireModelEvent('updating')) {
            if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) {
                return false;
            }
        }

        // First we need to create a fresh query instance and touch the creation and
        // update timestamp on the model which are maintained by us for developer
        // convenience. Then we will just continue saving the model instances.
        if ($this->usesTimestamps()) {
            $this->updateTimestamps();
        }

        // Once we have run the update operation, we will fire the "updated" event for
        // this model instance. This will allow developers to hook into these after
        // models are updated, giving them a chance to do any special processing.
        $dirty = $this->getDirty();

        if (count($dirty) > 0) {
            $this->setKeysForSaveQuery($query)->update($dirty);

            $this->syncChanges();

            $this->fireModelEvent('updated');
        }

        return true;
    }
}

4.4 SoftDeletes使用

namespace Hyperf\Database\Model;
abstract class Model implements ArrayAccess, Arrayable, Jsonable, JsonSerializable, CompressInterface
{
    public function delete()
    {
        $this->mergeAttributesFromClassCasts();

        if (is_null($this->getKeyName())) {
            throw new Exception('No primary key defined on model.');
        }

        // If the model doesn't exist, there is nothing to delete so we'll just return
        // immediately and not do anything else. Otherwise, we will continue with a
        // deletion process on the model, firing the proper events, and so forth.
        if (! $this->exists) {
            return;
        }

        if ($event = $this->fireModelEvent('deleting')) {
            if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) {
                return false;
            }
        }

        // Here, we'll touch the owning models, verifying these timestamps get updated
        // for the models. This will allow any caching to get broken on the parents
        // by the timestamp. Then we will go ahead and delete the model instance.
        $this->touchOwners();

        $this->performDeleteOnModel();

        // Once the model has been deleted, we will fire off the deleted event so that
        // the developers may hook into post-delete operations. We will then return
        // a boolean true as the delete is presumably successful on the database.
        $this->fireModelEvent('deleted');

        return true;
    }
}
namespace App1\Model;

use Hyperf\Database\Model\SoftDeletes;
/**
 */
class User extends Model
{
    use SoftDeletes;
}
namespace Hyperf\Database\Model;
trait SoftDeletes
{
    protected function performDeleteOnModel()
    {
        if ($this->forceDeleting) {
            $this->exists = false;

            return $this->newModelQuery()->where($this->getKeyName(), $this->getKey())->forceDelete();
        }

        return $this->runSoftDelete();
    }
     protected function runSoftDelete()
    {
        $query = $this->newModelQuery()->where($this->getKeyName(), $this->getKey());

        $time = $this->freshTimestamp();

        $columns = [$this->getDeletedAtColumn() => $this->fromDateTime($time)];

        $this->{$this->getDeletedAtColumn()} = $time;

        if ($this->timestamps && ! is_null($this->getUpdatedAtColumn())) {
            $this->{$this->getUpdatedAtColumn()} = $time;

            $columns[$this->getUpdatedAtColumn()] = $this->fromDateTime($time);
        }

        $query->update($columns);
    }
    public function getDeletedAtColumn()
    {
        return defined('static::DELETED_AT') ? static::DELETED_AT : 'deleted_at';
    }
}

你可能感兴趣的:(php,php,hyperf)