教程:Hyperf
一、命令行
根据之前应该能了解到命令行的基本实现,和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('<info>Model %s was created.</info>', $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';
}
}